Kotlin 44
Let’s see about converting the
when
to a table. Sounds easy enough.
Here’s the class in question:
data class Imperative(val verb: String, val noun: String) {
fun act():String {
val action:(Imperative) -> String = when(verb) {
"go" -> { _ -> "went $noun" }
"say" -> { _ -> "said $noun"}
"inventory" -> { _ -> "You got nothing"}
else -> { _ -> "I can't $verb a $noun"}
}
return action(this)
}
fun setNoun(noun: String): Imperative = Imperative(verb,noun)
}
This is used by the ImperativeFactory, which we may rename or cover in the future.
We’re just dispatching on the imperative’s verb. The obvious table is therefore from verb->lambda, but I think we’ll create an object. Let’s call it an ImperativeActions, signifying that it is a collection.
I’m not adept enough at Kotlin type definitions. I think what we want is a lambda from Imperative to Unit. We won’t return anything, we’ll just do things. And our imperative will do the doing. I’m guessing this is going to lead us to another Interface and some concrete classes, but we’ll see.
I’ll type in the table first. Internally, for now at least, I guess it has to be from verb (String) to lambda. Do I seem uncertain? I am.
I get to this, more slowly than I care to admit:
data class Imperative(val verb: String, val noun: String) {
var said: String = ""
fun act():String {
val lam = actionTable.getValue(this.verb)
lam(this)
return said
}
fun say(s:String) {
said = s
}
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}") }}
It took me a long time to sort out what that final lambda-containing lambda was whining about. Anyway, this is working. Now let’s imagine an ActionFinder object, or maybe Actions, and use it. I think I’ll rename the table while I’m at it. As you’ll see below:
data class Imperative(val verb: String, val noun: String) {
var said: String = ""
val actions = Actions()
fun act():String {
actions.action(this)(this)
return said
}
typealias Action = (Imperative) -> Unit
class Actions(private val verbMap:Map<String, Action> = actionTable) {
fun action(imperative:Imperative): Action {
return verbMap.getValue((imperative.verb))
}
}
This actually runs green. Commit: New Actions object handles Imperative action dispatch. Not even any warnings.
No reason to have the Imperative fetch the function and then call it. Let’s rename the action function performAction
and have it call the lambda.
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)
}
typealias Action = (Imperative) -> Unit
class Actions(private val verbMap:Map<String, Action> = actionTable) {
fun performAction(imperative:Imperative): Unit {
verbMap.getValue((imperative.verb))(imperative)
}
}
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}") }}
At this point, the entire processing of an Imperative, starting from either a single word (verb) or two words (verb, noun) consists of nothing more than table lookups. The work is done in ImperativeFactory, with some slightly subtle stuff going on with the convenient withDefault
feature of Kotlin.
Here’s ImperativeFactory for our amusement:
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)
}
Summary
If that isn’t nifty, I don’t know what is. I don’t even think it’s too cool to live. We’ll see what my colleagues think, and if you have a view, please let me know.
With this in place, we should be able to completely define a world’s behavior via the DSL. In a few days, we’ll start trying that out.
What remains to be done? Ideas include:
- More typealias or even classes to distinguish verb and noun from random strings. Even just declaring them would help.
- Make sure that all the tables can be injected or passed at instance creation as needed.
- Work out and build details of setting all this into the World.
- Probably more typealias work to define some of the more complex types that are turning up.
And, of course, plenty of general code improvement as we see opportunities.
For now, that’ll do. See you next time!