GitHub Repository

Let’s do a bit more on the SmartMap, and then try to get it in play.

If I look confused, it’s because in between opening this file, all ready to get started, and pasting in the blurb above, I was distracted by Google mail insisting on a password. I think it’s all sorted now, but I have to regather my thoughts.

Oh, right, SmartMap. It needs two capabilities in order to be useful from the DSL.

  1. It needs to have a method to clear the local map, which should be done before we start to do anything in a room, to clear out any leftovers from before.
  2. There needs to be a way to load the local map, which will be done after clearing.
  3. There needs to be a local map created by the DSL and saved in the room.

See? Whenever I think there are just two (or N) things, there’s always at least one more. Fascinating. Anyway, let’s build those capabilities using tests.

Digression: Why?
In another part of the forest1, an exchange with Clare Sudbery has got me thinking about why sometimes folx don’t write tests like these, or why they don’t refactor code that needs it. If you know some reasons that seem generally applicable to you, please tweet them to me, perhaps in a reply to my posting of this article. Thanks!

I write these tests for some2 reasons:

  1. They help me formulate the calling sequence for the thing I’m building. Often, until I get started, I don’t really have a good sense of what is needed.
  2. They help me experience what the user experiences when using the thing. Different from getting a calling sequence, it helps me get one that isn’t too much of a pain to use.
  3. They tell me whether the thing works. Even with all the help Kotlin and IDEA give me, I still make little mistakes that cause things not to work just as I intended.
  4. They protect me when I change things. The fundamental particle of software development is the change. It’s what we do, all day, every day. And when we make changes, sometimes things break, even things that seem far away. My tests don’t detect all my mistakes, but they do detect many of them.

These reasons all boil down to one: when I work this way, my work goes more smoothly, with less stress, more consistent progress, and fewer unhappy surprises. It just goes better when I do this.

Clearing the Locals

I think I can do well by just extending this test, which tells a story already:

    @Test
    fun `smart map`() {
        val g = mutableMapOf<String,String>(
   "go" to "went",
   "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf(
   "say" to "said"
        )
        val sm: SmartMap<String, String> = SmartMap(g,l)
        assertThat(sm.getValue("say")).isEqualTo("said")
        assertThat(sm.getValue("go")).isEqualTo("went")
        assertThat(sm.getValue("whazzup")).isEqualTo("I have no idea what whazzup is")
        assertThat(sm.getGlobalValue("say")).isEqualTo("global said")
    }

If I clear the local above and do the say again, I should get “global said”.

        sm.clearLocal()
        assertThat(sm.getValue("say")).isEqualTo("globalsaid")

I’ve intentionally made the message that comes back wrong, because I want to see it provide the right answer with my own eyes. First, I’ll put in an empty clearLocal. It should fail with “said” not “globalsaid”. And it does:

Expecting:
 <"said">
to be equal to:
 <"globalsaid">
but was not.

Now to fill it in. How do we remove all the entries in a map? Nice, the method is clear. So:

    fun clearLocal() {
        safeLocal.clear()
    }

I expect “global said” not matching “globalsaid”

Expecting:
 <"global said">
to be equal to:
 <"globalsaid">
but was not.

Perfect. Fix the test:

        sm.clearLocal()
        assertThat(sm.getValue("say")).isEqualTo("global said")

Perfect again. Commit: SmartMap has clearLocal.

As I was doing that, I realized that I don’t like the naming of safeLocal, because it makes the code look asymmetric. See what I mean:

class SmartMap<K,V>(private val global: MutableMap<K,V>, local: MutableMap<K,V>) {
    private val safeLocal = local.withDefault { key: K -> getGlobalValue(key) }
    fun getValue(key: K): V = safeLocal.getValue(key)
    fun getGlobalValue(key: K): V = global.getValue(key)
    fun clearLocal() {
        safeLocal.clear()
    }
}

It just seems to me that the working names should be local and global. It makes more sense. But I do like the names in the constructor being global and local. That makes sense. Kotlin lets me put it this way:

class SmartMap<K,V>(private val global: MutableMap<K,V>, local: MutableMap<K,V>) {
    private val local = local.withDefault { key: K -> getGlobalValue(key) }
    fun getValue(key: K): V = local.getValue(key)
    fun getGlobalValue(key: K): V = global.getValue(key)
    fun clearLocal() {
        local.clear()
    }
}

Yes, the private val local works, because the local in the constructor has no val or var and is therefore invisible in the methods, so it’s OK to create a property of the same name.

I think I like this better. Test: green. Commit: rename private safe local to local for code symmetry.

OK, now we need a way to add things to the local. (And we’ll need to add to the global as well. Might as well do them both. I am absolutely sure we’ll need the global one.)

Aside
Is adding them both a violation of the YAGNI3 guideline? Is it too speculative? You could make the case, and if you were here and said “let’s wait and see”, I’d go along with you, but because we know we’re going to allow creating global entries in the world part of DSL, as well as local ones in the room part of DSL, I feel confident that it’ll be OK.

OK, you talked me out of it. We’ll defer the global until we need it. It’ll show that we don’t need to do much grand design, even if we could. Incremental for the win. But we do need the local, that’s what we’re working on.

The existing test is too long, I’ll do a new one. I might even break up the old one a bit.

    @Test
    fun `can add to the local map`() {
        val g = mutableMapOf<String,String>(
   "go" to "went",
   "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf<String,String>(
        )
        val sm: SmartMap<String, String> = SmartMap(g,l)
        assertThat(sm.getValue("say")).isEqualTo("global said")
        sm.put("say", "said")
        assertThat(sm.getValue("say")).isEqualTo("said")
        sm.clearLocal()
        assertThat(sm.getValue("say")).isEqualTo("global said")
    }

The standard way to put things in a map is as above, with two elements, so I figure we’ll follow that pattern. We implement:

    fun put(key: K, value: V) {
        local.put(key, value)
    }

Green. Commit: SmartMap.put(k,v) adds k,v to local map.

Reflection
This is going well. What’s next? Well, we want to implement a new word in the DSL, probably action, that defines a new action in a room. And, it occurs to me, here’s what should have been in the itemized list above, we need to get the World working on SmartMap instead of regular maps. That’s down to Actions, I think.

There’s probably something clever to be done here, like subclassing SmartMap from map or an interface or something. I think we’ll go more old-school than that. We have no need for our Actions ever to accept simple maps, so let’s just make that class expect a SmartMap. Or, no, wait … what if we were to let it accept a regular map and wrap it in a SmartMap internally? That might be better. Let’s see how many places we create Actions?

There are only four places. I do think we want a SmartMap constructor that accepts only the global map and creates the local internally. That’ll be a good thing to learn how to do, I don’t think I’m clear on having more than one constructor. Let’s do another test:

    @Test
    fun `secondary constructor`() {
        val g = mutableMapOf<String,String>(
   "go" to "went",
   "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val sm: SmartMap<String, String> = SmartMap(g)
        assertThat(sm.getValue("say")).isEqualTo("global said")
        sm.put("say", "said")
        assertThat(sm.getValue("say")).isEqualTo("said")
        sm.clearLocal()
        assertThat(sm.getValue("say")).isEqualTo("global said")
    }

This is the same as the previous test, except that I only provide the global map to the constructor. IDEA objects and I’m sure it’ll put in the boilerplate for me … but no, it does not. No problem, I’ll do it myself:

No, wait! I can do this with a default value in the primary. That’s better. This does the job just fine:

class SmartMap<K,V>(
    private val global: MutableMap<K,V>, 
    local: MutableMap<K,V> = mutableMapOf<K,V>()) {

Super. That’ll make it much easier to convert the Action to expect a SmartMap. Let’s look at that code now:

typealias Action = (Imperative) -> Unit

class Actions(private val verbMap: MutableMap<String, Action>) {
    fun act(imperative: Imperative) {
verbMap.getValue((imperative.verb))(imperative)
    }

    fun put(action: Pair<String, (Imperative) -> Unit>) {
        verbMap.put(action.first, action.second)
    }
}

You know what? We can accept a vanilla map here and convert it internally, so that no one even has to know what we’re up to … at least not yet.

Musing
One concern with hiding information like this is that someone may someday need to do something clever, and there’ll be no way to reach inside and do whatever. I think we should expect that changes may be needed, and write our classes to hide as much information as possible, trusting that we’re smart enough to provide what’s needed when we need it. I suspect I’ve fallen into some bad habits during my work with Codea, where you can easily reach down inside objects. We’ll do this thing because no one needs to know how Actions work, just that they do.
class Actions(private val map: MutableMap<String, Action>) {
    private val verbMap = SmartMap(map)
    fun act(imperative: Imperative) {
verbMap.getValue((imperative.verb))(imperative)
    }

    fun put(action: Pair<String, (Imperative) -> Unit>) {
        verbMap.put(action.first, action.second)
    }
}

I notice that we have a put method on this class, and as written (now), it’s going to add things to the local map, because that’s how SmartMap defaults. Let’s first test, because I don’t think anything will break … yet. Right, everything still green. Now who’s doing these puts?

    fun addShout() {
        lexicon.actions.put("shout" to { imp: Imperative -> imp.say(
   "Your shout of ${imp.noun.uppercase()} echoes through the area.")},)
    }

This is the only one. I think we want put to default to global. Maybe it’ll be better to have only putLocal and putGlobal? Yes, since the SmartMap defaults to put things locally. Let’s be careful and clear, with putLocal and putGlobal. As much as I like hiding information, I think the dual nature of the SmartMap, and therefore the Actions, is important and needs to be clear in use.

Rename put in SmartMap to putLocal. (And, look, we’re going to need the global next. I told you we would.) Tests green. Commit: rename SmartMap.put to putLocal.

Now change the other put, in the shout code, to ask for putGlobal.

    fun addShout() {
        lexicon.actions.putGlobal("shout" to { imp: Imperative -> imp.say(
   "Your shout of ${imp.noun.uppercase()} echoes through the area.")},)
    }

IDEA tells me that I need a method of that name in Actions, and I can rename its put to link it up.

    fun putGlobal(action: Pair<String, (Imperative) -> Unit>) {
        verbMap.putGlobal(action.first, action.second)
    }

Oh, look, we need putGlobal in SmartMap? Who could have guessed that?4

class SmartMap ...
    fun putLocal(key: K, value: V) {
        local.put(key, value)
    }

    fun putGlobal(key: K, value: V) {
        global.put(key, value)
    }

IDEA wants to do those as assignments. I am OK with that, let’s see what it does:

    fun putLocal(key: K, value: V) {
        local[key] = value
    }

    fun putGlobal(key: K, value: V) {
        global[key] = value
    }

OK, makes sense to me. Test. Green. Commit: SmartMap has putLocal and putGlobal.

OK, this was quite the astounding flurry5 of activity. Are we ready now to work on the action word in the room DSL?

I’ve been heads-down here for just shy of two hours. Do I need a break? Probably. Shall I take one? Not yet. Let’s look at what it might take to add something to our actions.

Let’s see how we do a go in the DSL. Our action will probably be similar. Here’s an example:

room("clearing") {
   desc("You're in a clearing",
       "You're in a charming clearing. There is a fence to the east.")
   item("cows")
   go("n", "wellhouse")
   go("s","clearing")
   go("e", "cows") {
       it.say("You can't climb the fence!")
       false
   }

We want two parameters to our action, a verb (String) and an Action. I think I’ll define the method first, hoping that the type declarations will help me.

Could it be this easy? I define this val and method:

    val actions = mutableMapOf<String,Action>()
    fun action(verb: String, action: Action) {
        actions[verb] = action
    }

Let me put an example into my playable world.

Warning: I’ve dropped into Spike mode. I’m fumbling my way to find everything that needs to be done. There’s more than I thought.

I’ll need to clear the actions locals and then add in mine. Actions doesn’t know how to do that yet.

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        world.lexicon.actions.clear()
        world.lexicon.actions.putAllLocal(actions)
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        val imp = Imperative(imperative.verb,imperative.noun,world, this)
        imp.act(world.lexicon)
    }

So I’ll put those two methods on Actions, and push the putAll into SmartMap.

With that accomplished:

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> e
You're in a pasture with some cows.

> cows
Leave those cows alone
You're in a pasture with some cows.

> w
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> cows
unknown command 'cows none'
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

Well. This nearly works. What we have learned, however, is that if we are going to define a word in some room, we’d better give it an overall meaning as well. And/or, our message about not understanding needs to be more like something the game would say, like “I don’t understand cows” or something like that.

I need a break. Tests are green. Commit: initial implementation or room.action. Needs refinement, support for new words not in global actions when not in the room implementing the new verb.

Let’s sum up. No … not yet

Pushing My Luck

After a short delay … I knew I was tiring and needed a break, but took a chance that what was needed was clear enough in my mind, and close enough to ready, that I could find my way to the end. Turns out I was correct. Or, more likely, lucky.

Here’s all it took, given that Actions was already using the SmartMap, which we did above.

class Room ...
    val actions = mutableMapOf<String,Action>()

    fun action(verb: String, action: Action) {
        actions[verb] = action
    }

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        world.lexicon.actions.clear() // new
        world.lexicon.actions.putAllLocal(actions) // new
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        val imp = Imperative(imperative.verb,imperative.noun,world, this)
        imp.act(world.lexicon)
    }

class Actions ...
    fun clear() {
        verbMap.clearLocal()
    }

    fun putAllLocal(actions: MutableMap<String, (Imperative) -> Unit>) {
        verbMap.putAllLocal(actions)
    }

class SmartMap ...
    fun putAllLocal(actions: MutableMap<K, V>) {
        local.putAll(actions)
    }

Clearly I should implement methods on lexicon or world or both to deal with setting up the local actions. Let’s do that now, before I forget. I think I’m up to it.

// Room
    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        world.defineLocalActions(actions)

// World
    fun defineLocalActions(actions: MutableMap<String, (Imperative) -> Unit>) {
        lexicon.defineLocalActions(actions)
    }

// Lexicon
    fun defineLocalActions(newActions: MutableMap<String, (Imperative) -> Unit>) {
        actions.defineLocalActions(newActions)
    }
}

//Actions
    fun defineLocalActions(actions: MutableMap<String, (Imperative) -> Unit>) {
        clear()
        putAllLocal(actions)
    }

Green. Commit: tidying Demeter issue from room to actions.

OK. I made about four mistakes doing that. Definitely tired. Still, I think the change is righteous unless there’s a more compact way of telling Kotlin how to trace down that chain from world to lexicon to actions to smart map.

Oh. I can at least do this sort of thing:

    fun defineLocalActions(actions: MutableMap<String, (Imperative) -> Unit>) 
    = lexicon.defineLocalActions(actions)

And really, why not a typealias for that kind of map?

typealias Action = (Imperative) -> Unit
typealias ActionMap = MutableMap<String, Action>

class Actions(map: ActionMap) {

// World
    fun defineLocalActions(actions: ActionMap) 
    = lexicon.defineLocalActions(actions)

// Lexicon
    fun defineLocalActions(newActions: ActionMap) 
    = actions.defineLocalActions(newActions)

I think those are a bit more tidy. Now let me stop before I do some damage.

Summary

The new feature, ability to define room-specific actions in the DSL is in and working! There is work yet to be done to ensure that words defined in one room don’t cause problems in the rooms where they’re not defined. Possibly we should allow a second action, optional, to be added to the globals? That way you could define a special action for the cows, plus a generic message, all in one place.

We’ll keep that in mind, shall we? In any case we need to cater to unique words’ handling in the rest of the world.

The feature went in very smoothly, and I think that was in large part due to the many simple changes that have gone before, in a few preceding articles, learning how to do the SmartMap idea, encapsulating it into a class, sliding that class into Actions, and so on.

Many More Much Smaller Steps

I think that the large lesson is still Many More Much Smaller Steps, as GeePaw Hill puts it, but those steps were all chosen as moves toward a bigger picture, the action command in the DSL. I would not argue that any random person could have followed that thread as happened here. Not that I’m a genius, which certainly assumes facts not in evidence. But I do have a very simple design here, by dint of continually trying to keep it simple, and I do have a lot of tests. Still, was there some magical element of understanding or insight that went into how well this went? I don’t know.

Magic? There is no magic!

If there was, the magical element is almost certainly made up of past experience, which, if we’re reasonably attentive, we all pick up over time. Had I not had quite the vision that I had with this action idea, I’d have worked more top down, from action in the DSL, pushing on down through the world, the lexicon, and so on. It might have turned out the same, after refactoring. Whether we’d have come up with the SmartMap idea … I’d like to think so, but maybe that was particularly creative.

Not modesty, he has none.

I hesitate to credit myself with particular creativity, and I don’t want anyone who wouldn’t have thought of it to feel discouraged. When we pay attention to the small bits in our code, we make it better. How far we push in making it better is a matter of choice. Choice, of course, is best done based on experience. And experience … that’s what we get from trying lots of things.

So, if I gave advice, I might suggest that readers try lots of things, accept that many of them won’t be quite right, and build up lots of experience. But I don’t give advice. I just do things and show you what happens.

Make up your own advice, that’s my motto6.



  1. Borrowed from GeePaw, borrowed from Shakespeare. 

  2. No point guessing how many, I’ll get it wrong anyway … 

  3. “You’re Not Gonna Need It”, a little mantra we say to remind ourselves not to build what we think we’ll need, but what we actually do need. It’s just good supply chain management, really. 

  4. Yes, this time we could have ignored the YAGNI-nagni. It wouldn’t have saved us any time because doing it here took just the same code as it would have before. YAGNI is right more often than it’s wrong, in my experience. It helps keep our changes small. 

  5. Subtle reference to the original Adventure game. 

  6. A motto for every occasion, that’s my motto.