I get good advice from my betters. In the face of that, I forge ahead.

In an astounding flurry of responses to yesterday’s work:

GeePaw Hill

GeePaw Hill takes me aside privately to tell me that my extension on String troubles him. He has mentioned this before, which is a sign that I am clearly not working out as a developer on his project unless and until I improve my ways. He prefers a typealias for Strings known to be special. Uses it for “keys” in his own work. The effect, to the compiler, is the same, but the code expresses itself better. I think I’ll try that.

Ward Cunningham

Even better, Ward Cunningham advised: “Lambda functions are almost production rules. Return true if the input matches. False otherwise.” It has been my privilege to receive advice from Ward a number of times, and it follows a common pattern. I think it may be, in fact, a deliberate pattern on his part. Or, it could just be me.

Almost invariably, when Ward has offered me an idea—and I’ve observed this with other folx lucky enough to get his advice—his first offering doesn’t take. Since games are on my mind this morning, it’s like the first puzzle clue in a game: it’s not very direct. You know how the clue sequence for some puzzle goes:

  1. You’re getting warmer.
  2. Where is that heat coming from?
  3. Look in the oven!

When Ward is coaching or helping someone, he tends to guide very lightly. This gives the individual every opportunity to discover as much of the idea for themselves as possible. It’s a beautiful and very kind art form, offering everyone he helps the joy of discovery on their own. I have tried to get better at doing that, because I really do want to be kind as well as helpful1, but after too many years of being the first student to put up his hand with an answer, it’s hard for me. I admire Ward’s style all the more because I can’t do it very well.

And it’s just very kind. How can you not feel good when someone is kind to you? (Even if it is a bit confusing.)

That said, I am exactly where Ward’s first bit of advice usually places me, looking around and wondering where lambda expressions might fit into what I’m doing. Let me share my thoughts: we might write some code this morning.

Parsing Thoughts

Yesterday I was blathering about not liking the fact that I have to write things like this:

    "e","w" -> move(cmd, world)
    "take axe" -> take("axe", world)

I don’t like repeating the , world part, because I forget. I was also observing that I keep forgetting that each of my methods there, move, take, and so on, must return the name of the room to which you are to be after the command is done. So I was already thinking about how to change that code so that I don’t have to type that repeated calling sequence, and I don’t have to remember to return a room name.

I think I have a solution, and in fact it involves lambda expressions. I swear I had at least the glimmer of this idea before Ward hinted. In fact, I think I mentioned yesterday that there’s a way to return (the name of) a function. Yes, I did.

So here’s my cunning2 plan. I’ll try it in my parsing spike to begin with. I’ll devise a calling sequence that will work for all commands, probably

    abcd(noun, verb, world)

Because that’s all the information we have, so it’s surely enough.

I’ll either write methods like move and take that we have now, and put their names in the command’s when patch, or I’ll put the code directly in the command patch as a lambda. Either way I’ll return the function rather than call it, and then call it outside the when.

And!! Thinking about this gives me another idea along the lambda line of thinking. Right now, the command when looks like this:

        val name = when(cmd) {
            "take axe" -> take("axe", world)
            "take bottle" -> take("bottle", world)
            "take cows" -> take("cows", world)
            "inventory" -> inventory(world)
            "s","e","w","n" -> move(cmd, world)
            "xyzzy" -> move("xyzzy", world)
            "cast wd40" -> castSpell("wd40", world)
            else -> "unknown cmd $cmd"
        }

We’re checking the input command for equality with a known command, and the when does the first one that matches. It turns out that the Kotlin when syntax is quite flexible and can in fact include any expression on the left, and if it’s true, then the thing on the right is done. That means that we could have a rack of lambda expressions doing whatever level of sophisticated parsing we might need.

That may or may not be what Ward was thinking about. I don’t know if he even looked at my code. But an idea, perhaps like his, perhaps not, has taken root in my head. See how nice that is?

Let’s experiment in my Spike ParsingTest. There’s a lot there, and I think we need to at least scan most of it. Part of what we’ll do this morning is to try to make it smaller and simpler.

class ParsingTest {
    @Test
    fun `pattern splitter`() {
        val regex = Regex("\\W+")
        val words = "take axe".split(regex)
        assertThat(words.size).isEqualTo(2)
        assertThat(words[0]).isEqualTo("take")
        assertThat(words[1]).isEqualTo("axe")
    }

    @Test
    fun `identify a few commands`() {
        val words = "take axe".parse()
        assertThat(words.size).isEqualTo(2)
    }

    @Test
    fun `some commands`() {
        var result:String
        result = command("e")
        assertThat(result).isEqualTo("move e")
        result = command("sw")
        assertThat(result).isEqualTo("move sw")
        result = command("ns")
        assertThat(result).isEqualTo("I don't understand ns.\n")
        result = command("take axe")
        assertThat(result).isEqualTo("Taken: axe")
    }
}

private fun command(cmd:String): String {
    val words = cmd.parse()
    if (words.size == 0) return "no command found"
    val verb = words[0]
    var noun = ""
    return if (words.size == 1)
        oneWord(words[0])
    else if (words.size == 2)
        twoWords(words[0], words[1])
    else
        "Too many words!"
}

fun oneWord(verb: String): String {
    return when(verb) {
        "n","s","e","w" -> move(verb)
        "nw","sw","ne","se" -> move(verb)
        "up","dn","down" -> move(verb)
        else -> "I don't understand $verb.\n"
    }
}

fun twoWords(verb: String, noun: String): String {
    return when(verb) {
        "take","get" -> take(noun)
        else -> "I don't understand $verb $noun.\n"
    }
}

private fun move(dir: String): String = "move $dir"

fun take(noun:String): String = "Taken: $noun"

private fun String.parse(): List<String> {
    return this.split(Regex("\\W+"))
}

Let’s go in tiny steps. First, let’s try returning the function and then executing it outside the when. It won’t save us much in the present form. I’ll do oneWord just to try out the effect.

fun oneWord(verb: String): String {
    val doer =  when(verb) {
        "n","s","e","w" -> ::move
        "nw","sw","ne","se" -> ::move
        "up","dn","down" -> ::move
        else -> { return "I don't understand $verb.\n" }
    }
    return doer(verb)
}

Here we return the function to be called, either move, designated by ::move because in this spike it’s not in a class, or by the lambda that returns the error message. Do I ever test for that? I surely should. Yes, it’s checked in some commands.

Probably doer isn’t a great word. Maybe operation? Or action? Yes, let’s go with action.

fun oneWord(verb: String): String {
    val action =  when(verb) {
        "n","s","e","w" -> ::move
        "nw","sw","ne","se" -> ::move
        "up","dn","down" -> ::move
        else -> { return "I don't understand $verb.\n" }
    }
    return action(verb)
}

I’m not quite sure how to put an expression on the left side here.

OK, after some experimentation, I’ve found that this works:

fun oneWord(verb: String): String {
    val cardinal = listOf<String>("n","s","e","w")
    val ordinal = listOf("nw", "sw", "ne", "se")
    val action =  when(verb) {
        in cardinal -> ::move
        in ordinal -> ::move
        inVertical(verb) -> ::move
        else -> { return "I don't understand $verb.\n" }
    }
    return action(verb)
}

fun inVertical(verb:String): String {
    val isIn = verb in listOf("up", "dn", "down")
    if (isIn) return verb else return ""
}

The left side function, isVertical returns verb if it is one of the ones it likes, and “” otherwise, which (I guess) is then compared with verb and found either equal or not, returning that ::move or not. Short form: a function in that position has to return a thing of the type of the when parameter.

We could of course do an array of functions returning booleans and use an if cascade. That would match Ward’s hint.

Just to document that, I’ll change twoWords to work that way, if I can manage it. Given how simple it is, this will not make it better, but it will teach me something. Let’s do it slowly. First, from this:

fun twoWords(verb: String, noun: String): String {
    return when(verb) {
        "take","get" -> take(noun)
        else -> "I don't understand $verb $noun.\n"
    }
}

To this:

fun twoWords(verb: String, noun: String): String {
    return when(verb) {
        "get" -> take(noun)
        "take"-> take(noun)
        else -> "I don't understand $verb $noun.\n"
    }
}

Then to do the indirect call action trick, just because I think we’ll really want to go with that:

fun twoWords(verb: String, noun: String): String {
    val action = when(verb) {
        "get" -> ::take
        "take"-> ::take
        else -> { return "I don't understand $verb $noun.\n" }
    }
    return action(noun)
}

Then change this to an if cascade:

fun twoWords(verb: String, noun: String): String {
    val action: (String)->String =
        if (verb == "get") {
            ::take
        } else if (verb == "take")
            ::take
        else
            { return "I don't understand $verb $noun.\n" }
    return action(noun)
}

Tests are green. Now we have a form where we could put more complex expressions, on the left. Even now, I don’t see that we’d prefer lambdas, but we certainly could use them. Let’s try that:

fun twoWords(verb: String, noun: String): String {
    val isGet:(String)->Boolean = { it == "get"}
    val isTake:(String)->Boolean = { it == "take"}
    val action: (String)->String =
        if (isGet(verb)) {
            ::take
        } else if (isTake(verb))
            ::take
        else
            { return "I don't understand $verb $noun.\n" }
    return action(noun)
}

Now we can try a more complex is. Let’s improve isGet.

fun twoWords(verb: String, noun: String): String {
    val isGet:(String)->Boolean = { it in listOf("get","take","fetch", "grab") }
    val action: (String)->String =
        if (isGet(verb)) {
            ::take
        } else
            { return "I don't understand $verb $noun.\n" }
    return action(noun)
}

So that’s interesting. We can see that isGet could be as complicated as we wish, and that we could indeed pass in the full command string to a cascade like this, with very advanced functions parsing away.

I’m not at all sure where we’ll go with this, be we’ve learned a lot of possibilities. And as this is Sunday, learning is more than we require of ourselves here chez Ron3. I’ll try to sum up.

Summary

We’ve determined that the complicated calling sequence that I’ve had embedded in my when can be “extracted”, removing the duplication of the argument lists, by returning the function, rather than calling it. We’ve seen that we can name the function via ::take, or return it explicitly via {...}. We didn’t see but I am sure that we can refer to a method as this::take or something similar in our real use of this parsing idea.

We’ve seen that we can make a cascade of boolean functions returning true if they accept a command, and there are other tricks that come to mind as well.

Suppose we made an ordered list of such functions, and used the Kotlin first predicate to choose one, something like this sketch:

    val preds = listOf(doMagic, doGet, doMove, doError)
    preds.first(it(cmd))

I think something like that would run through the list until one of the preds returns true, having accepted and processed cmd. We’d have to be careful to be sure that some pred would accept anything. Like doError above, which I have in mind says “???” and returns true.

Today’s exercise, inspired by my friends, has caused me to try a lot of things, and get ideas for even more. We’ll see, over the next day or two, how I mess them up.

Oh, before I forget, let me apply Hill’s idea in my ParsingTest.

typealias Command = String

private fun Command.parse(): List<String> {
    return this.split(Regex("\\W+"))
}

private fun command(cmd:Command): String {
    val words = cmd.parse()
    if (words.size == 0) return "no command found"
    val verb = words[0]
    var noun = ""
    return if (words.size == 1)
        oneWord(words[0])
    else if (words.size == 2)
        twoWords(words[0], words[1])
    else
        "Too many words!"
}

There, that was easy. One final commit: Using typealias Command = String.

And that’ll do, folks! See you next time!



  1. And good-looking, it should go without saying. 

  2. Yes, I did that. Sorry not sorry. 

  3. There he goes with the French again.