Kotlin 45
Let’s put this in context. No, I’m not explaining. We’re going to put all this into Context.
- Project Repo
- Some folx have asked if there’s a repo for this. There is. Feel free to browse around, grab a copy, whatever. Even raise issues, I guess. I probably won’t accept pull requests, I’m confused enough with my own code. I will look at them, though.
- Apologia Pro Vita Sua
-
If there is a general topic to all my innumerable programming posts, it is to explore the evolution of ideas and code together, in small steps. Whether we’re working toward a conscious “grand design” or not, we try always to start with a running program, and to keep it running thereafter, generally delivering more capability on a regular cycle, from beginning to end.
-
The reason why I focus on regular delivery of visible capability is that I think it is an important aspect to keeping the customer satisfied, where by customer I mean whoever needs to be satisfied that work is progressing as it should be. In the Manifesto’s Principles, we said “working software is the primary measure of progress”. Of my many past failures, I think that most could have been averted, or softened, had I known enough to follow that guideline.
-
There are two main reasons why I work in small steps, with an evolving and therefore always not quite adequate design. First, to show what a development effort might look like when begun and continued in the above style. The second, perhaps more important, is to show, repeatedly, that faced with a design that isn’t quite the thing, that we can improve it, to our benefit, in small steps.
-
No, three main reasons1 for smalls steps. The most important: I produce working code faster, with more ease and confidence, when I proceed in tiny steps supported by tests. I do this because it works so well for me.
- : Most of the time, most programmers are probably working with a design that isn’t quite the thing, and I want to provide them with a bit of confidence that they can make their work life a bit better, bit by bit.
-
There are more important quests2 in life, but this one, at least in these pages, is mine.
Today’s Plan …
It’s kind of a joke to talk about a plan when your whole approach to the work is opportunistic, but I do usually have an idea about what I’m going to work on, and where it’s going to go. It’s just that, as Kent Beck said, I like to let the code participate in my designing, and it usually has a pretty firm opinion about what should be done. And, of course sometimes I just see a Shiny Squirrel3 and can’t help chasing it.
When I started toward the computer this morning, I was thinking that I’d build some kind of language object to contain the three objects (so far) that are used in creating the Imperatives that I now plan(!) to put into the actual game. It just made more sense to me to package them together into a single object, since they’re always going to be used that way. And then … my plan changed. I’m already “planning” to have a CommandContext
and already have an interface and a concrete example:
interface CommandContext {
val directions: List<String> // {e, east, w, west, up, dn, down ...}
val verbsTakingNoNoun: List<String> // {inventory, look }
val magicWords: List<String> // {xyzzy, plugh }
val operations: Map<Verb,(Command)->String> // {take->(), ...}
}
And look what’s already in there … language stuff. I haven’t even gotten around yet to putting the world or room or game or whatever we’ll need in there. So “clearly”, I was thinking until about a dozen words ago that we’d put our new objects into the CommandContext. But that was about twenty words ago. Now I “see” that the context needs to have at least two aspects, the language, and the world. I do think we’ll find it best to package them together … but I also think it’d be wise to keep the language stuff, and the world stuff, separate, within the context object.
So now I am back to Plan A, with an added expectation about how I’ll use the language object. Flexible planning? Unnecessary vagueness? I think of it as thinking all the time, and always trying to move in a favorable direction.
So, let’s rig up a little language object. Since it is basically made of words, let’s call it a Lexicon, and let’s see how convenient we can make it to create, and to use.
Getting Oriented.
We’re about to make some structural changes to our code, so let’s get oriented. In our Imperative Spike, which I think we’ll evolve into production code, we have three tables:
val actionTable = mapOf(
"go" to { imp:Imperative -> imp.say("went ${imp.noun}")},
"say" to { imp:Imperative -> imp.say("said ${imp.noun}")},
"inventory" to { imp:Imperative -> imp.say("You got nothing")}
).withDefault {it ->{imp:Imperative -> imp.say("I can't ${imp.verb} a ${imp.noun}") }}
val synonymTable = mapOf(
"e" to "east",
"n" to "north",
"w" to "west",
"s" to "south").withDefault { it }
val imperativeTable = mapOf(
"go" to Imperative("go", "irrelevant"),
"east" to Imperative("go","east"),
"west" to Imperative("go","west"),
"north" to Imperative("go","north"),
"south" to Imperative("go","south"),
"say" to Imperative("say", "irrelevant"),
"xyzzy" to Imperative("say", "xyzzy"),
).withDefault { (Imperative(it, "none"))
}
OK, synonyms, imperatives, actions. Decent names, probably. In prod, these tables, or the objects that cover them, will get used in two ways. During game play, they will be read by the command code. During game setup, via the DSL, these same objects will be updated with words defined in the DSL. In fact, we may find that we want the tables to be dynamic during game play, with words or actions changing as the player “moves” and “acts” in the world. OK, either way, the objects that we’ve begun to create can surely deal with both read and write access.
What are those objects? We have three very tiny objects holding these collections:
class VerbTranslator(private val map:Map<String,Imperative>) {
fun translate(verb:String): Imperative = map.getValue(verb)
}
class Synonyms(private val map: Map<String,String>) {
fun synonym(word:String) = map.getValue(word)
}
typealias Action = (Imperative) -> Unit
class Actions(private val verbMap:Map<String, Action> = actionTable) {
fun performAction(imperative:Imperative) {
verbMap.getValue((imperative.verb))(imperative)
}
}
Are you wondering about such tiny objects?
The first two of those do nothing other than provide a different word from getValue
to fetch their result. And the third just gets the value and then calls it. Are these guys carrying their weight? In my view, they are, even now, if only because of the simplicity of the code that uses them.
These one-line methods do all the parsing from one- or two-word phrases to an Imperative:
class ImperativeFactory(private val verbTranslator:VerbTranslator, private val synonyms:Synonyms = Synonyms(synonymTable)) {
constructor(map: Map<String, Imperative>) : this(VerbTranslator(map))
fun create(verb:String): Imperative = imperative(verb)
fun create(verb:String, noun:String) = imperative(verb).setNoun(synonym(noun))
private fun imperative(verb: String) = verbTranslator.translate(synonym(verb))
private fun synonym(verb: String) = synonyms.synonym(verb)
}
The Imperative itself is also simple (and we see a glimmer of a need for the context object in it):
data class Imperative(val verb: String, val noun: String) {
var said: String = ""
val actions = Actions()
fun act():String {
actions.performAction(this)
return said
}
fun say(s:String) {
said = s
}
fun setNoun(noun: String): Imperative = Imperative(verb,noun)
}
The variable said
and method say
are really test-only. We’ll probably want to get rid of them in due time.
Wondering
I’m considering next steps. I could just go ahead and create the Lexicon, then see what seems next. Or, I could work with Imperative, plugging in some kind of context and grow what we need from there.
I think we’ll start with the Lexicon: I have a somewhat more clear vision of how to do that. I’ll start with a test.
As soon as I write this much, I see an issue:
@Test
fun `create a lexicon`() {
val vt = VerbTranslator(imperativeTable)
val actions = Actions(actionTable)
val synonyms = Synonyms(synonymTable)
}
It would seem that the names of the covering objects are not consistent, nor are the underlying tables. Let’s do a bit of renaming. Let’s rename VerbTranslator to Verbs, and imperativeTable to verbTable. IDEA will help with that.
Two quick applications of Shift-F6 (obvious, really) and the renaming is done. I rename my local variable in the test, too, then extend it to require the new object.
@Test
fun `create a lexicon`() {
val synonyms = Synonyms(synonymTable)
val verbs = Verbs(verbTable)
val actions = Actions(actionTable)
val lexicon = Lexicon(synonyms, verbs, actions)
}
I re-ordered the temps to match what I’m intending for the constructor. IDEA thinks I want a new class. I tend to agree, so I’ll let it create one. So far, I’m keeping everything in the test file. I’ll move it to the main side in due time.
Wow, this thing is smart. It created this:
class Lexicon(synonyms: Synonyms, verbs: Verbs, actions: Actions) {
}
Perfect, I couldn’t have done better myself, and I might have been tempted to use shorter names. These are better.
Now we want to cover the individual actions of the inner objects with methods on Lexicon, so that users just deal with it.
@Test
fun `create a lexicon`() {
val synonyms = Synonyms(synonymTable)
val verbs = Verbs(verbTable)
val actions = Actions(actionTable)
val lexicon = Lexicon(synonyms, verbs, actions)
assertThat(lexicon.synonym("e")).isEqualTo("east")
}
IDEA wants to help. However, this time, it guesses wrong about what I want, offering:
class Lexicon(synonyms: Synonyms, verbs: Verbs, actions: Actions) {
fun <ELEMENT> synonym(s: String): MutableList<out ELEMENT>? {
}
}
I don’t even know what that means. No harm done, I’ll do it myself. In doing it myself, I realize that all the constructor parameters need to be val, so that I can see them. (What good are they otherwise, I wonder?)
class Lexicon(val synonyms: Synonyms, val verbs: Verbs, val actions: Actions) {
fun synonym(word:String):String = synonyms.synonym(word)
}
I expect green. Green it is. Commit: Initial Lexicon.
After recovering from accidentally typing Cmd-Q, let’s extend the test:
@Test
fun `create a lexicon`() {
val synonyms = Synonyms(synonymTable)
val verbs = Verbs(verbTable)
val actions = Actions(actionTable)
val lexicon = Lexicon(synonyms, verbs, actions)
assertThat(lexicon.synonym("e")).isEqualTo("east")
val imp: Imperative = lexicon.translate("e")
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
}
Let’s give IDEA another shot at creating that method. Much better guess this time:
fun translate(word: String): Imperative {
TODO("Not yet implemented")
}
I’ll fill in my bit:
fun translate(word: String): Imperative = verbs.translate((word))
Again I expect green. I am mistaken, seriously so.
Expecting:
<"e">
to be equal to:
<"go">
but was not.
Perhaps I’d be wise to look and see what the Verbs object really does.
Ah. It expects that I’ll have done synonyms first. I forgot to do that. Change the test:
val imp: Imperative = lexicon.translate(
lexicon.synonym("e")
)
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
Perfect. Green. Commit: Lexicon has translate method.
Now we need to do action. We have an Imperative so we should be good to go.
val imp: Imperative = lexicon.translate(
lexicon.synonym("e")
)
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
assertThat(imp.act()).isEqualTo("went east")
No, wait, that’s no good. As things stand, Imperative creates its own actions and asks it to act:
data class Imperative(val verb: String, val noun: String) {
var said: String = ""
val actions = Actions()
fun act():String {
actions.performAction(this)
return said
}
We need to create these guys with at least a Lexicon, and ultimately, a Context. And I’d really prefer not to have to rewrite all my tests. So let’s give Imperative another construction parameter and default it so that the tests will run. This will work, for now, but probably not as long as we’d like: when we move the objects to the prod side, some of this may break.
Because I’m trying to learn IDEA as well as Kotlin, as well as how to write this game, I’ll try change signature on this:
data class Imperative(val verb: String, val noun: String) {
Working together, we get this:
data class Imperative(val verb: String, val noun: String, val actions: Actions=Actions()) {
fun act():String {
actions.performAction(this)
return said
}
I’m a bit confused because of a few quick turns that IDEA put me through, but the test will get me oriented. Probably. A couple of tests fail. This surprises me. I’ll have a quick look.
Before I even look, I realize that I don’t want to pass in Actions, I want to pass in the Lexicon. I should, and will, revert, but first I can’t resist looking at the tests.
Expecting:
<Imperative(verb=go, noun=east, actions=com.ronjeffries.adventureFour.Actions@4351c8c3)>
to be equal to:
<Imperative(verb=go, noun=east, actions=com.ronjeffries.adventureFour.Actions@3381b4fc)>
but was not.
Ah. The Imperative is a data class, and it generated an equality test, and I’ve used it. Adding the parameter broke it.
We could override equals. We could inject the actions table. We could pass it in on the act(). Let’s try that. Revert. Add to the test again:
val imp: Imperative = lexicon.translate(
lexicon.synonym("e")
)
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
assertThat(imp.act(lexicon)).isEqualTo("went east")
Give act an optional parameter? I guess for now, that’s what we need to do. No, I decide that since all my tests are local, I’ll just pass it in unconditionally, and fix up the tests.
data class Imperative(val verb: String, val noun: String) {
fun act(actions:Actions):String {
actions.performAction(this)
return said
}
I note another issue, the return said
, and our special handling of say
inside the class, used in testing. We’ll come back to that. Let’s get this working.
IDEA has lit up about a thousand red lines, representing all the things that are wrong now. This is almost useful.
Darn! I’m wrong again. We don’t want to give the actions method the Actions, we want to give it the lexicon. Change the code and test to reflect this.
I back up to go again. The test:
val imp: Imperative = lexicon.translate(
lexicon.synonym("e")
)
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
assertThat(imp.act(lexicon)).isEqualTo("went east")
In my defense, right before I got confused up there, I took a short break to walk down the hall4. Naturally upon my return a few minute later, I need retraining. Let’s make the lexicon a required parameter in act. We’ll change this:
data class Imperative(val verb: String, val noun: String) {
val actions = Actions()
fun act():String {
actions.performAction(this)
return said
}
To this:
data class Imperative(val verb: String, val noun: String) {
fun act(lexicon: Lexicon):String {
lexicon.act(this)
return said
}
My plan is to make this work and then see if I like it. I’m not certain that I will, but I want to see it, not just imagine it.
class Lexicon(val synonyms: Synonyms, val verbs: Verbs, val actions: Actions) {
fun synonym(word:String):String = synonyms.synonym(word)
fun translate(word: String): Imperative = verbs.translate((word))
fun act(imperative: Imperative) = actions.act(imperative)
}
The Actions current method is performAction
so I’ll rename that.
class Actions(private val verbMap:Map<String, Action> = actionTable) {
fun act(imperative:Imperative) {
verbMap.getValue((imperative.verb))(imperative)
}
}
I think this is going to be the wrong parameter to pass, unless the Imperative knows it context already. We’ll see what happens. There’s no need to decide this now. The code will tell us what it wants in due time. For now, we want to get this thing working again, and there are those million errors. IDEA will give me a list and I rather like that.
They all say “no value passed for parameter lexicon”. A typical case:
@Test
fun `imperative can act`() {
val imperatives = ImperativeFactory(Verbs(verbTable))
var imp: Imperative = imperatives.create("east")
assertThat(imp.act()).isEqualTo("went east")
imp = imperatives.create("e")
assertThat(imp.act()).isEqualTo("went east")
imp = Imperative("forge", "sword")
assertThat(imp.act()).isEqualTo("I can't forge a sword")
}
I think I’ll create a handy function testLex
to deal with this. That might suffice.
@Test
fun `imperative can act`() {
val imperatives = ImperativeFactory(Verbs(verbTable))
var imp: Imperative = imperatives.create("east")
assertThat(imp.act(testLex())).isEqualTo("went east")
imp = imperatives.create("e")
assertThat(imp.act(testLex())).isEqualTo("went east")
imp = Imperative("forge", "sword")
assertThat(imp.act(testLex())).isEqualTo("I can't forge a sword")
}
fun testLex(): Lexicon {
val synonyms = Synonyms(synonymTable)
val verbs = Verbs(verbTable)
val actions = Actions(actionTable)
return Lexicon(synonyms, verbs, actions)
}
I have more act
calls to fix the same way. Done, and delightfully, all the tests now pass. Commit: Lexicon supports act.
Time to relax and reflect.
Reflection
Overall, I feel that this is shaping up nicely, and it has proceeded without much confusion on my part, except for the unfortunate need for retraining after my walk down the hall. I do feel that there’s an issue with how this is evolving. It seems to me that parts of the current design are ahead of others. Further along. Not quite lined up right:
I think what should probably happen next is to take what we have here, and to plug it into the actual game. That is likely to be a bit messy, but we’ll see how smoothly we can do it if we seek out small steps.
But for today, we’ve improved the structure a bit, our objects are tiny, and all our tests are running. Maybe the odds are in our favor.
See you next time!
-
Obligatory nod to the Spanish Inquisition sketch. ↩
-
Our world needs some work. People are treated horribly. The planet itself is being destroyed by our wasteful actions. We have almost unlimited power to do good, and yet we do so poorly, as if we must fight for everything. Quests in this context are more important. I do what I can to help the world, as well. It’s just that my thing, mostly, is software. ↩
-
World’s most distracting object, the Shiny Squirrel. There’s one now!
↩
-
Metaphor? Euphemism? Exercise? Any two out of three? Stop reading this, get back to work! ↩