Kotlin 11
In which your intrepid author suddenly realizes something, and decides to do something he rarely does.
TL;DR
We are building a tree of various class instances by calling methods on the parent classes that build instances of child classes under themselves. As we do that …
The syntax name {someCode}
means “call the function name
, passing as its only parameter the function defined by {someCode}
.”
Long Form
- Added in Post
- What follows is long, convoluted, prolix, prolonged, protracted, arguably laborious in the extreme, and seemingly endless. If you read it, you are reading the written thoughts of someone trying to understand something well enough to explain it briefly. The TL;DR is the result. What follows is how I got there.
When I wake up in the morning, at first my thoughts are random. Then, typically, they turn to whatever I’ve been working on. Sometimes, I’m just musing and might even fall back to sleep. A good thing, if it’s early in the morning. Sometimes my demons1 start talking to me, and I get up to do some work to drive them away. But sometimes, the work idea is particularly interesting, and I get right up to work on it. Some of my best articles have come from these early morning alerts.
I make no such claim for today’s article, but I was driven to get up by a sudden realization. Something I knew, but had not had explicit in my head, nor in my writing about the DSL builder that was the subject of Kotlin 10. I have not thought clearly, and therefore have not written clearly, about what our little DSL is actually doing.
This is such an oversight that I’m thinking I might go back and make an “Editor’s Note” in Kotlin 10. I sometimes go back to prior articles to fix typos and such, but rarely truly revise them, because the point of most of my programming articles is to show my then-current understanding, and to show how, even with such a flawed and incomplete grasp of what the heck I’m even doing, I can generally evolve code that becomes better and better rather than worse and worse.
In such a style, a revision breaks the fundamental principle of showing how software can evolve well. But this, I think, needs some kind of additional emphasis.
Wow, I’ve built this insight up way beyond what it is worth. I’m going to feel let down now by sharing it. But, here we are.
The point of our DSL is to build a tree, no less and [usually] no more.
What is a tree? A tree, abstractly, is a connected set of nodes starting from one identified node, the “root”. For reasons, we usually draw the root at the top. Each node has (is connected to) one or more additional nodes, and so on, down to a final set of nodes that have no children. These final nodes are called leaves. We refer to parent nodes and child nodes as well.
What are the nodes? Well, they can be anything: the essential property is just that each node has zero or more child nodes. The nodes could be all the same kind of thing, or all different kinds of things, so long as they all can have children.
In our case, the nodes are all instances of different classes. There is an HTML node, a Head node, a Body node, and so on, something like this:
an HTML
a Head
a Text
a Body
a Heading
a Text
a Text
Now, in Kotlin 10, I did mention that we’re building a tree, but I wasn’t really very clear about it, because I didn’t have a clear statement in my mind, something like
OK now folx, we’re going to write a little language that will help us build a tree of object instances, such as, viz., e.g., …
That’s what we’ve done, but I’ve not been clear. I hope that if you read Kotlin 10 you saw what was going on despite my lack of clarity. And, I guess my overall point remains: we can make good progress with limited understanding, so long as we increase our understanding and express it in our code.
Let’s look at our DSL in that light. We have a Kotlin statement that looks like this:
html {
head {
title {
text("big Title")
}
}
body {
h1 {
text("some heading")
}
p {
text("some sentences ...")
}
}
}
After that statement runs, we want to have a data structure that looks like this:
an HTML instance
a Head instance
a Title instance
a Text instance
a Body instance
an H1 instance
a P instance
And the question we probably have, the question that I certainly had when I first saw that Kotlin statement, is “what even is that statement?” I literally could not parse it. I didn’t know what, say, body
was, nor what body {...}
even meant. Let’s begin with a test that will, I hope, clarify my thinking and maybe even yours.
Begin with this test:
@Test
internal fun createMumbles() {
val m = Mumble(2,4) {a + b}
val six = m.doIt()
assertThat(six).isEqualTo(6)
}
Our mission with this test is to understand that odd notation after the creation, with the weird {a + b]
bit. Other than that, the test calls for a class Mumble with a method doIt
. Thus:
class Mumble(val a:Int, val b:Int, val f: Mumble.()->Int) {
fun doIt(): Int {
return f(this)
}
}
The class has three input parameters, a, b, and f. And when we call its doIt member function, it returns the value of f(this), that is, passing the current member as a parameter to the function f.
This test passes. Our first question is probably still “What’s that syntax??” but right on its heels is “How did that work??”.
The syntax is a special deal in Kotlin. If the final parameter on a constructor is a function, you can end the parameter list and then append the function in brackets. So the {a + b}
bit is the f
parameter, which is why, when we call it, we get 6, the sum of 2 and 4, the arguments we passed in.
We can extend the test:
@Test
internal fun createMumbles() {
val m = Mumble(2,4) {a + b}
val six = m.doIt()
assertThat(six).isEqualTo(6)
val m2 = Mumble(2,4) {a*b}
val eight = m2.doIt()
assertThat(eight).isEqualTo(8)
}
That passes, because 2 times 4 is 8, despite two plus four being six.
There is some way to define the function elsewhere and pass it in, but I’m not going to look that up, because here we have what we need. One more underlining observation. Note the signature of the class definition:
class Mumble(val a:Int, val b:Int, val f: Mumble.()->Int)
The type of the function is Mumble.()->Int. What does that mean? Well, it means that it returns Int, we’re good there. But what is the Mumble.()
saying? As I understand, as yet weakly, that’s saying that this is a function in the context of a Mumble instance, and therefore it can refer to Mumble’s visible members, in this case, a and b. We could write the function like this:
val m = Mumble(2,4) {this.a + this.b}
Because the function is defined as Mumble.()
, this
means whatever Mumble instance we have when we call f
, namely m
for our first call and m2
for the second.
One more thing, or maybe two. We can do the same thing with methods. If a method’s final parameter is a function, we can pass it outside the parens. Let’s do another test.
@Test
internal fun functionWithFunctionParameter() {
val m = Mumble(3,4) {0}
val nine = m.process() {a*a}
assertThat(nine).isEqualTo(9)
}
Here we give Mumble a function returning 0 for its f parameter but we are calling a new method, process
and we’ve put a function after that call. We define process
like this:
fun process(g: Mumble.()->Int) : Int {
return g()
}
Again its parameter is a function in the context of a Mumble, returning an Int. It returns its result by calling the parameter function g. And, again, we don’t have to put the function inside the parens, we can append it, in {}
after the call.
One more thing. (I was mistaken when I said that earlier.) We can remove the empty parens on the call, changing this:
val nine = m.process() {a*a}
To this:
val nine = m.process {a*a}
Thus we see that in the snippet
html {
head {
title {...}
}
body {
h1("Big Header")
}
}
The words html
, head
, body
, and title
are all method function calls with only one parameter, a function, which is provided, instead of inside the call’s parens, outside, in {}
.
To review in the light of today’s understanding, each word in the DSL tree is the name of a method in some class, namely the class mentioned above. body
is a method on the HTML class, title
is a method on body
. By convention, each of those methods will create a class instance implied by its name. body
will create an instance of Body
. title
will create an instance of Title
.
- Full Disclosure
-
I’ve used a lot of words here to describe what is really a simple idea. If the final parameter to a function call )or object constructor) is a function, you can put the function definition outside the normal parentheses of the function call, in curly brackets
{}
instead. And, if there are no other parameters, you can omit the parentheses entirely. -
I’ve used up so many words because I’m still pounding the idea into my own head. Perhaps one day I’ll be able to do a four line succinct answer to the question “What are those words with brackets after them anyway?”. Today is not that day. It takes us a while from using boilerplate, to modifying it, to being able to generate it, to having a basic understanding of how it works, to finally getting it. I’m coming closer to getting it.
I’m going to wrap this article here, and maybe do another one today, or maybe not.
I hope you’re way ahead of me. If you’re not, I hope you’re at least about even with me:
Summary (TL;DR)
We are building a tree of various class instances by calling methods on the parent classes that build instances of child classes under themselves. As we do that …
The syntax name {someCode}
means “call the function name
, passing as its only parameter the function defined by {someCode}
.”
-
The usual: old age, Covid, climate, the downfall of civilization. You know, the silly stuff we worry about sometimes. ↩