Kotlin 36
It’s 0525. I want to begin to try something before actually starting my day. This may go somewhere interesting, or crumble to sand in my hands.
My influences this morning include:
- Ward Cunningham
- Ward added a few lines to his remark about lambdas, remarking that he wrote functions matching valid input, wrapped them with “data collection proxies”, which gave him an abstract syntax tree, and with some added tracing to help with reasoning. This sort of lit a lamp in my mind.
- Philip Schwarz
- Philip wrote “When you get to the point where you use Monads in Kotlin, then the notion of a monadic parser is very interesting”. He provided a link to an introduction. I replied “I don’t understand monads and I’m not sure I want to. The only reason I’d try would be in hopes of becoming the first person to explain them to normal people.”
-
I wasn’t entirely facetious there, because I’ve never found an explanation of monads that would fit in my brain, and I hope I’m not the only remaining human who feels that way. I did go on to watch another couple of videos about monads and to read a bit about them.
- GeePaw Hill
- GeePaw chimed into the conversation and his comments drew out some of what’s above. He may have also said something that’s influencing me directly, but if he did, I’ve forgotten it now.
So those are my influences. Let me be clear: I am not planning to implement, much less explain monads, nor, really, to implement some lambda-driven parser to produce an abstract syntax tree. What I do plan to do is to try some learning experiments in Kotlin, aimed at parsing my simple ADVENTure language, and to see where it leads. I’ll start by trying to refine the waking thoughts that have driven me to my computer before I’ve even really begun my day.
The Command Process
The original game had essentially a two-word command language, verb noun, like “take bottle” or “throw axe”. It allowed quite a few one word commands as well, like “east” or “xyzzy” or “plugh”. In my little Kotlin experiment, I’m working on a DSL for describing a game like that, and I do not intend to move past two-word commands.
So I was thinking of transforming (parsing) the commands, along these lines:
- Split the input into a list of words.
- If there’s only one word, try to transform to two.
- With two words, verb noun, look up the operation for the verb.
- Execute that operation, passing the noun as its parameter.
My plan for the one-word commands is that they all turn into two-word commands. “East” becomes “go east”, and “xyzzy” becomes “say xyzzy”. That sort of thing.
It would be nice if, during that process, we could produce a few reasonable error messages when something goes wrong.
If the above sounds a bit like what Ward solved, or a bit like what a monadic parser could do, I suspect you’re on the right track. But I’m not aiming for that. I am aiming for what’s above, armed with decades of experience, a focus on doing small things, and the words of those influencers in my head.
A Tentative “Design”
I’m envisioning a new object, a (valid) Command, containing a verb and a noun. The command starts with a string, no verb, and no noun, and we transform it repeatedly until it is either a valid Command or an error Command, signified perhaps by the verb “error”. Or “3RR0R” so that no one will type it. I don’t know, just blue-skying here.
I propose to write some tests and code leading me in the direction above, and to see where it takes me. But first, some justification:
Why Is It OK to Do This?
If I were developing an ADVENture product, I’m not sure that I could justify what I’m about to do, because it is seriously easy to parse the commands out manually, with a few spit
or substring
operations, and we could do that and get on with the product. But I’m not doing a product: I’m learning Kotlin. As such, this digression is almost certain to teach me useful things about Kotlin and the world.
In a real product, it’s harder to justify a learning effort, and my own approach to dealing with that tension is not one that I can entirely recommend: I have spent much personal time learning, while doing programming for pay; I have spent too much time learning for pay when people thought I should be programming for pay; I have failed to deliver because people ran out of patience. In other words, I often was not delivering visible value fast enough to keep people from, well, giving up on me, or even the product.
I’d focus more on a continual flow of features, if I were programming for a living today. But here, with this effort, I can dial down the features and dial up the learning. I’m working at home, from home, and I’m pretty much the only person I have to please.
It’s a very desirable position, and aside from the zero income aspect, it’s one that I enjoy. I have worked hard, failing often, to get here.
Let’s get to it.
A New Test File
I already have a file “ParsingTest”, and I intend to keep all these little experimental files. So I need a decent name for the new file … uh … get off the dime … CommandExperiment.
class CommandExperiment {
@Test
fun `valid command`() {
}
}
Right. Where to start? I think that a Command begins with a string and ends up with a verb and a noun, so let’s imagine a Command class:
class CommandExperiment {
@Test
fun `valid command`() {
val command = Command("go east")
command.validate()
assertThat(command.verb).isEqualTo("go")
assertThat(command.noun).isEqualTo("east")
}
}
OK, it’s a beginning. I’ll write a private class here in the test file.
private class Command(val input: String) {
var verb = ""
var noun = ""
val words = mutableListOf<String>()
fun validate(){
words += input.split(" ")
verb = words[0]
noun = words[1]
}
}
I really think I want the verb and noun private, and there’s probably some well-known Kotlin way to do that, but I’m not willing to divert to work out what it is. Something about get()
but I have too many dishes spinning already to look it up.
Anyway the test runs. But I’m thinking of a list of operations, make list of words, make the list two long, make sure the verb is legal, look up the verb, execute the command. Suppose we had functions for each of those, and that each function applied to a command and produced a command. How might we write that? Maybe something like this:
@Test
fun `sequence`() {
val command = Command("take axe")
command
.makeWords()
.makeVerbNoun()
.findOperation
assertThat(command.operation).isEqualTo(::take)
}
I’m envisioning those .things as all methods on Command, returning a command.
fun makeWords(): Command {
words += input.split(" ")
return this
}
Something like that.
fun makeVerbNoun(): Command {
expandIfNeeded()
verb = words[0]
noun = words[1]
return this
}
I’m writing a lot of code here. I don’t see anything simpler to do, but each thing is easy so far, I think. I believe I can make this work …
After a bit more scrambling than is elegant, I have this much:
@Test
fun `sequence`() {
val command = Command("take axe")
command
.makeWords()
.makeVerbNoun()
.findOperation()
var result = command.execute()
assertThat(result).isEqualTo("axe taken.")
}
Supported by all this:
private class Command(val input: String) {
var verb = ""
var noun = ""
val words = mutableListOf<String>()
var operation = ""
var result: String = ""
fun validate(){
words += input.split(" ")
verb = words[0]
noun = words[1]
}
fun makeWords(): Command {
words += input.split(" ")
return this
}
fun makeVerbNoun(): Command {
expandIfNeeded()
verb = words[0]
noun = words[1]
return this
}
fun expandIfNeeded(): Command {
return this
}
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = "::take"
return this
}
fun take(noun:String) {
result = "$noun taken."
}
fun commandError() {
result = "command error"
}
fun execute(): String {
if (operation == "::take") {
take(noun)
return result
} else
return "unknown operation"
}
}
The test runs. This is a good thing. And my brain is tired. I really shouldn’t be up this early and with no fuel. Let me take a break.
I wanted operation
to contain a method name, but haven’t been able to sort out the syntax yet. I’ll try again.
A few bites of banana and a small amount of hammering, and my test runs, in this form:
@Test
fun `sequence`() {
val command = Command("take axe")
command
.makeWords()
.makeVerbNoun()
.findOperation()
var result = command.execute()
assertThat(result).isEqualTo("axe taken.")
}
private class Command(val input: String) {
var verb = ""
var noun = ""
val words = mutableListOf<String>()
var operation = this::commandError
var result: String = ""
fun validate(){
words += input.split(" ")
verb = words[0]
noun = words[1]
}
fun makeWords(): Command {
words += input.split(" ")
return this
}
fun makeVerbNoun(): Command {
expandIfNeeded()
verb = words[0]
noun = words[1]
return this
}
fun expandIfNeeded(): Command {
return this
}
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
return this
}
fun take(noun:String) {
result = "$noun taken."
}
fun commandError(noun: String) {
result = "command error"
}
fun execute(): String {
operation(noun)
return result
}
}
Now it’s “easy to see” that we could have done all this with lambda expressions, along the lines that Ward suggested. Each of the parsing methods here could be written explicitly as a lambda, saved in a table or written longhand. For now, I prefer what we have here.
It seems that we can now “just” add harder tests and enhance these methods to do a better job.
First, let’s change this:
fun makeVerbNoun(): Command {
expandIfNeeded()
verb = words[0]
noun = words[1]
return this
}
Let’s move the expandIfNeeded
into the test and remove it from here. That should make no difference, because:
fun expandIfNeeded(): Command {
return this
}
@Test
fun `sequence`() {
val command = Command("take axe")
command
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
var result = command.execute()
assertThat(result).isEqualTo("axe taken.")
}
This should still run. If it does, I’d better commit before I forget. Yes. Commit: Initial CommandExperiment.
Let’s do another two-word command as our next test.
@Test
fun `go command`() {
val command = Command("go east")
command
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
var result = command.execute()
assertThat(result).isEqualTo("went east.")
}
We test expecting a failure.
Expecting:
<"command error">
to be equal to:
<"went east.">
but was not.
Perfect. We need a go command.
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
return this
}
fun go(noun) {
result = "went #noun."
}
I expect green. I get an error requiring this:
fun go(noun: String) {
result = "went #noun."
}
Now then, green? No, sorry, did you see the typo above?
fun go(noun: String) {
result = "went $noun."
}
Green now??? Yes. Commit: CommandExperiment understands go
.
I’m committing and pushing despite some warnings. I’ll mention them when I decide to fix them, but I do look to be sure they aren’t interesting.
It’s 0730, I’ve been at this for two hours, but the banana is beginning to hit, so I am less out of it. I see some duplication in the tests, this bit:
command
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
var result = command.execute()
First, we can change to this:
@Test
fun `go command`() {
val command = Command("go east")
val result = command
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
.execute()
assertThat(result).isEqualTo("went east.")
}
We have one other test that’s different, this one:
fun `valid command`() {
val command = Command("go east")
command.validate()
assertThat(command.verb).isEqualTo("go")
assertThat(command.noun).isEqualTo("east")
}
That was a scaffolding test to get us going, and it’s covered by the one just above, so I’ll delete the old one.
The parsing sequence is fixed. Let’s fold it into a method. Since I just removed the need for validate, let’s change it and use it.
@Test
fun `sequence`() {
val command = Command("take axe")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("axe taken.")
}
@Test
fun `go command`() {
val command = Command("go east")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("went east.")
}
And, of course:
fun validate(): Command{
return this
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
}
We should be green. We even are. Commit: validate method encapsulates full command parsing sequence.
Now let’s do a harder test. There are some single-word commands that need to be converted to verb-noun form. east->go east, xyzzy->say xyzzy. We’ll suppose that all the words, including magic ones, are predefined.
I am already realizing that I want another method. Hang in there, you’ll see what I mean. Here’s a starting test:
fun `single word commands`() {
val command = Command("east")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("went east.")
}
We need to enhance our expandIfNeeded
method:
fun expandIfNeeded(): Command {
if (words.size == 2) return this
val directions = listOf(
"n","e","s","w","north","east","south","west",
"nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
"up","dn","down")
val word = words[0]
if (word in directions) {
words[1] = words[0]
words[0] = "go"
}
return this
}
We are green. Commit: direction commands convert to go.
Let’s expand the tests in two ways, a magic word, and an unknown one. First:
fun `single magic word commands`() {
val command = Command("xyzzy")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("said xyzzy.")
}
And, clearly:
fun expandIfNeeded(): Command {
if (words.size == 2) return this
val directions = listOf(
"n","e","s","w","north","east","south","west",
"nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
"up","dn","down")
val magicWords = listOf("xyzzy", "plugh")
val word = words[0]
if (word in directions) {
words[1] = words[0]
words[0] = "go"
} else if (word in magicWords) {
words[1] = words[0]
words[0] = "say"
}
return this
}
This method is getting to be about 15 times longer than my preferred length. We’ll see what we can do about that. Meanwhile, let’s handle an error.
fun `single unknown word commands`() {
val command = Command("fragglerats")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("I don't understand fragglerats.")
}
I could have committed above. Too late now. Test because I want to see that error: it amuses me.
Arrgh. I’ve made a rookie mistake! I didn’t prefix my tests with @Test and now a bunch of them are failing. Doubleplusungood. I could roll back or roll forward. Let’s try forward first.
Before that, I must feed the cat. And make the morning iced chai.
OK, now then, what are these errors about? They’re getting index out of bounds. One is here:
if (word in directions) {
words[1] = words[0]
words[0] = "go"
That’s my Lua talking. In Lua I could just do that. In Kotlin, I have to add to the list. I bet there’s a list.insert … not quite, but there is add(index.item). OK.
This should fix part of it:
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
}
Unfortunately, not all. single magic word
says:
Expecting:
<"command error">
to be equal to:
<"said xyzzy.">
but was not.
And single unknown word
fails assigning word[1] here:
fun makeVerbNoun(): Command {
verb = words[0]
noun = words[1]
return this
}
That’ll be because the words didn’t get expanded. Let’s do this:
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
} else {
words.add(0, "I don't understand")
}
This is going to get me further into error handling than I want. Let’s get the tests closer to running, though.
single unknown word
says
Expecting:
<"command error">
to be equal to:
<"I don't understand fragglerats.">
but was not.
And single magic word
says:
Expecting:
<"command error">
to be equal to:
<"said xyzzy.">
but was not.
I suspect a flaw in my test for magic words. A typo, perhaps? I don’t see the problem. Here’s the code for expanding from one word to two:
fun expandIfNeeded(): Command {
if (words.size == 2) return this
val directions = listOf(
"n","e","s","w","north","east","south","west",
"nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
"up","dn","down")
val magicWords = listOf("xyzzy", "plugh")
val word = words[0]
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
} else {
words.add(0, "I don't understand")
}
return this
}
The go
test has worked, which tells me, I think, that the word in
is doing what I thought, and so is add(0,...
. So what has gone wrong here?
I refuse to learn how to run the debugger, as a matter of principle. Let’s write a test for expandIfNeeded
. Step by step, first this:
@Test
fun `expand magic`() {
val command = Command("xyzzy")
command.makeWords()
assertThat(command.words.size).isEqualTo(1)
}
Assert passes. Then:
@Test
fun `expand magic`() {
val command = Command("xyzzy")
command.makeWords()
assertThat(command.words.size).isEqualTo(1)
command.expandIfNeeded()
assertThat(command.words.size).isEqualTo(2)
assertThat(command.words[0]).isEqualTo("say")
assertThat(command.words[1]).isEqualTo("xyzzy")
}
We’ll see what that says. Since junit only handles one assertion at a time, we may have to comment some of these out. We’ll see.
Ha! It passes. I think the bug is that I haven’t recognized “say” as a verb yet, so we throw the error from there. If I’m write, verb and noun should be OK in the magic test. Let’s get in there and check them.
command.makeVerbNoun()
assertThat(command.verb).isEqualTo("say")
assertThat(command.noun).isEqualTo("xyzzy")
I expect this to continue to pass. I could have gone for the fix, but making the test stronger seems better. It still runs. So we look now into findOperation
:
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
return this
}
Right. No say
. Fix that:
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
else if (verb == "say")
operation = ::say
return this
}
fun say(noun:String) {
result = "said $noun."
}
Test. Expect better errors, and maybe even fewer of them. Yes just one
single unknown word commands
Expecting:
<"command error">
to be equal to:
<"I don't understand fragglerats.">
but was not.
What has happened? Well, when we tried to expand the single word message and failed, we said this:
} else {
words.add(0, "I don't understand")
And when we tried to look up the command in findOperation
, we do not find it, so we just return, and we initialize operation to:
var operation = this::commandError
Which is this:
fun commandError(noun: String) {
result = "command error"
}
We need much better error handling. In our simple case, we don’t need to do much. Let’s just implement one more command, verbError
, and use it like this:
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
} else {
words[0] = "verbError"
words.add(word)
}
return this
And then let’s understand verbError
:
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
else if (verb == "say")
operation = ::say
else if (verb == "verbError")
operation = ::verbError
return this
}
And we implement:
fun verbError(noun: String) {
result = "I don't understand $noun"
}
Test. This is probably close.
Expecting:
<"I don't understand fragglerats">
to be equal to:
<"I don't understand fragglerats.">
but was not.
Perfect! Fix:
fun verbError(noun: String) {
result = "I don't understand $noun."
}
I should run green. I do. Commit: CommandExperiment understands directions and magic words, plus take, go, say.
I think we’re where I want to be, or nearly so. Let’s take a look at the whole Command class, and see about tidying it up a bit.
private class Command(val input: String) {
var verb = ""
var noun = ""
val words = mutableListOf<String>()
var operation = this::commandError
var result: String = ""
fun validate(): Command{
return this
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
}
fun makeWords(): Command {
words += input.split(" ")
return this
}
fun expandIfNeeded(): Command {
if (words.size == 2) return this
val directions = listOf(
"n","e","s","w","north","east","south","west",
"nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
"up","dn","down")
val magicWords = listOf("xyzzy", "plugh")
val word = words[0]
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
} else {
words[0] = "verbError"
words.add(word)
}
return this
}
fun makeVerbNoun(): Command {
verb = words[0]
noun = words[1]
return this
}
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
else if (verb == "say")
operation = ::say
else if (verb == "verbError")
operation = ::verbError
return this
}
// execution
fun execute(): String {
operation(noun)
return result
}
fun commandError(noun: String) {
result = "command error"
}
fun go(noun: String) {
result = "went $noun."
}
fun say(noun:String) {
result = "said $noun."
}
fun take(noun:String) {
result = "$noun taken."
}
fun verbError(noun: String) {
result = "I don't understand $noun."
}
}
We can fold up the result = stuff by having all those methods return the string:
fun execute(): String {
result = operation(noun)
return result
}
And change all the command guys, first like this:
fun go(noun: String): String {
return "went $noun."
}
And then like this:
fun go(noun: String): String = "went $noun."
That gives us this:
fun commandError(noun: String) : String = "command error"
fun go(noun: String): String = "went $noun."
fun say(noun:String): String = "said $noun."
fun take(noun:String): String = "$noun taken."
fun verbError(noun: String): String = "I don't understand $noun."
Nice. Still green. Commit: Tidying execution functions to single expression body.
How let’s convert this to a when:
fun findOperation(): Command {
val test = ::take
if (verb=="take")
operation = ::take
else if (verb == "go")
operation = ::go
else if (verb == "say")
operation = ::say
else if (verb == "verbError")
operation = ::verbError
return this
}
I wonder if IDEA can help me with this. After quite some searching, I don’t see any refactoring suggestions that make sense. I’ll do it the old-fashioned way.
But when I position the cursor in front of the if, it pops up “Cascaded if should be replaced with when”. Finally I hold my tongue just right and it gives me this:
fun findOperation(): Command {
val test = ::take
when (verb) {
"take" -> operation = ::take
"go" -> operation = ::go
"say" -> operation = ::say
"verbError" -> operation = ::verbError
}
return this
}
Now can I get it to move the operation =
bit outside? Again I can’t find anything. I’ll just do it:
I think it didn’t want to do it because it doesn’t know if the list is exhaustive. I do this:
fun findOperation(): Command {
val test = ::take
operation = when (verb) {
"take" -> ::take
"go" -> ::go
"say" -> ::say
"verbError" -> ::verbError
else -> ::commandError
}
return this
}
Test. Green. Do we have a test that gets the command error?
@Test
fun `evoke command error`() {
val command = Command("vorpal blade")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("command error 'vorpal blade'.")
}
fun commandError(noun: String) : String = "command error '$input'."
We’re green. Commit: Implemented command error.
I’ll put this whole file in an appendix. Let’s sum up.
Summary
So this was interesting. In mostly very tiny steps, we now have a parsing sequence for our simple commands:
fun validate(): Command{
return this
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
}
When we execute that, it dispatches to defined methods like take
and go
, and to some standard error methods, all of which just return a string result. That’s not fancy enough for general parsing, but with our very simple language, we don’t even need any branching in the syntax tree.
There is one slightly nasty trick in there. When the expandIfNeeded
detects a one-word message and cannot expand it to two words, it sets the first word to “verbError” and the second word to the input word. Then makeVerbNoun
knows “verbError” as a legal verb and converts it to ::verbError
, which emits the error message.
If I were a good person, which assumes facts not in evidence, I would handle these errors in a more standard way. The current favorite among the cognoscenti is probably Either
, which is part of the Functional Programming repertoire that we’re all supposed to know. Down that path lies monads. Well, I’m new around here, so I’m not up to that yet, but we’ll see, next time, about improving the error handling of our parser to reflect more modern thinking. I don’t know exactly, or even approximately, what I’ll do but I think it’ll be something like this:
We’ll create an object with two distinct states, an OK one and an ERROR one. As we go down our parsing methods, makeWords, expand, and so on, as long as everything is fine, we’ll return an OK to the next guy. If something goes wrong, like too many words (which we don’t even handle yet) or an unknown single word, or an unknown verb, we’ll instead return an ERROR, and subsequent parsing methods will just skip on by. Probably.
This may lead somewhere interesting. If we’re incredibly lucky, it’ll lead to a monadic error handling example, without ever having had the need to say A monad is just a monoid in the category of endofunctors, which really let’s face it no one should ever say outside of an advanced math class.
I don’t know if we’ll get there. I’m just a boy trying simple changes to make things better and to figure out how to do that in Kotlin.
Other than that, we have that one rather long method, expandIfNeeded
. That needs improvement, and, glancing at it, I think it’s really hiding a more complex parsing situation, since it has conditionals in there. I’m sure we can do better. I’ll make a note of it.
Still, I think we got a fairly clean little example here. I hope you’ll come back to see what happens next!
Appendix: Code
package com.ronjeffries.adventureFour
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.reflect.KFunction
class CommandExperiment {
@Test
fun `sequence`() {
val command = Command("take axe")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("axe taken.")
}
@Test
fun `go command`() {
val command = Command("go east")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("went east.")
}
@Test
fun `expand magic`() {
val command = Command("xyzzy")
command.makeWords()
assertThat(command.words.size).isEqualTo(1)
command.expandIfNeeded()
assertThat(command.words.size).isEqualTo(2)
assertThat(command.words[0]).isEqualTo("say")
assertThat(command.words[1]).isEqualTo("xyzzy")
command.makeVerbNoun()
assertThat(command.verb).isEqualTo("say")
assertThat(command.noun).isEqualTo("xyzzy")
}
@Test
fun `single word go commands`() {
val command = Command("east")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("went east.")
}
@Test
fun `single magic word commands`() {
val command = Command("xyzzy")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("said xyzzy.")
}
@Test
fun `single unknown word commands`() {
val command = Command("fragglerats")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("I don't understand fragglerats.")
}
@Test
fun `evoke command error`() {
val command = Command("vorpal blade")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("command error 'vorpal blade'.")
}
}
private class Command(val input: String) {
var verb = ""
var noun = ""
val words = mutableListOf<String>()
var operation = this::commandError
var result: String = ""
fun validate(): Command{
return this
.makeWords()
.expandIfNeeded()
.makeVerbNoun()
.findOperation()
}
fun makeWords(): Command {
words += input.split(" ")
return this
}
fun expandIfNeeded(): Command {
if (words.size == 2) return this
val directions = listOf(
"n","e","s","w","north","east","south","west",
"nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
"up","dn","down")
val magicWords = listOf("xyzzy", "plugh")
val word = words[0]
if (word in directions) {
words.add(0,"go")
} else if (word in magicWords) {
words.add(0,"say")
} else {
words[0] = "verbError"
words.add(word)
}
return this
}
fun makeVerbNoun(): Command {
verb = words[0]
noun = words[1]
return this
}
fun findOperation(): Command {
val test = ::take
operation = when (verb) {
"take" -> ::take
"go" -> ::go
"say" -> ::say
"verbError" -> ::verbError
else -> ::commandError
}
return this
}
// execution
fun execute(): String {
result = operation(noun)
return result
}
fun commandError(noun: String) : String = "command error '$input'."
fun go(noun: String): String = "went $noun."
fun say(noun:String): String = "said $noun."
fun take(noun:String): String = "$noun taken."
fun verbError(noun: String): String = "I don't understand $noun."
}