GitHub Repo

Fundamental theorem or not, implementation inheritance the tool of the devil or not, what we have is better than anything I can foresee. Full speed ahead! (Three false starts, then success.)

There are half or six a dozen ways that our space objects can be called, to do their thing. Update, interaction (begin, with, with other, end), draw, finalize. Because we have implementation defaults in the hierarchy, a given object only needs to implement the methods it cares about. That keeps each space object’s source code as simple as it can be, and the calling code makes no special decisions: it sends all the messages to everyone, and they deal with the message or ignore it as they may prefer.

I worked on my small experiment where an object would provide a table of the functions to call and the game cycle would only call the ones in your table. That scheme, I figured out, would require all the called methods to take the same parameter list, and they don’t all need the same list and would have to ignore some of the parameters. Not good. I’m not even sure if I could get around that, but if I could, it would involve building some “generic” objects, classes that proliferate to allow versions of themselves that accept different classes as input or output. That’s pretty deep in the bag of tricks, and while I might like to know how to use them, today is not that day.

My dear friend GeePaw Hill has devised a scheme that allows objects to “register” for the things they want to do, and to be called only if they have registered for that thing. It kind of turns the Game’s cycle of calls into a cycle of triggering events. His scheme is now in the repo both as a branch and in the main as SmalltalkTest, because I brought it in in order to try to understand how it works. Frankly, after a few hours’ work, I get the drift of how it works, but I couldn’t explain it coherently, and I absolutely could not replicate it if I needed to. It is, I think, a tour de force1 of making a language with strict typing behave like one with duck typing, at least if the ducks were all willing to tell you in advance what they’d do for you. Hill’s scheme is amazing and I commend it to your study. And it is so deep in the bag of tricks that my fingers can’t even grasp it to pull it out.

So therefore … I’m going to belay the search for a scheme that doesn’t use implementation inheritance, see whether we can make this thing any simpler and therefore more clear, and then push forward with new features.

Accumulating Parameter: Transaction

Some good ideas came out of my tête-à-tête2 with Hill on this issue. I think most of them were his ideas: he’s surely a much better programmer than I am.

One of his good ideas is this. Many of the system’s operations currently return a Transaction. Hill changed, at one point, to a scheme in which the caller of the method passes in a Transaction. That simplifies the method’s signature and allows it to be simpler, as it never has to create a Transaction. I think this is a better idea. I also think it may break some tests, but that’s to be expected when you get a better idea.

Start #1

Let’s start with … update. Here’s what we know about update:

abstract class SpaceObject {
    open fun update(deltaTime: Double): Transaction { return Transaction() }

We’re on a green bar with no code hanging to commit. Perfect time to try changing this. IDEA offers a Change Signature refactoring, we’ll try that.

It’s not much help, but we wind up with this:

    open fun update(deltaTime: Double, transaction: Transaction) { }

I’m sure that there’s a way to get a list of everyone who needs to change.

Here’s one:

    fun tick(deltaTime: Double): Transaction {
        elapsedTime += deltaTime
        return update(deltaTime,)
    }

I think we should have started with tick. The Game goes like this:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
    }

    fun tick(deltaTime: Double) {
        knownObjects.applyChanges(cumulativeTransactionFromUpdates)
        cumulativeTransactionFromUpdates.clear()
        knownObjects.forEach { cumulativeTransactionFromUpdates.accumulate(it.tick(deltaTime)) }
    }

We’ll let update lie and work here. Currently cumulativeTransactionFromUpdates is a val in Game, into which we dump the returned transactions from our calls, and which we then clear right here. All that to save recreating a Transaction? Let’s make it a var and use it a bit differently as we go forward.

Start #2

I think I’ll revert first, and start from here.

Yes, since the refactoring didn’t know how to update the methods who are going to receive a transaction, there were compile errors waiting to pop up. We’ll start here:

class Game {
	...
    private var cumulativeTransactionFromUpdates = Transaction()

    fun tick(deltaTime: Double) {
        knownObjects.applyChanges(cumulativeTransactionFromUpdates)
        cumulativeTransactionFromUpdates = Transaction()
        knownObjects.forEach { cumulativeTransactionFromUpdates.accumulate(it.tick(deltaTime)) }
    }

This is the same except that we are creating a new transaction after applying the old one. I just like that better than relying on clear. I’m not sure why. Now let’s pass the transaction to tick instead of returning it:

Can I rename that long name, please? How about cycleTransaction?

    fun tick(deltaTime: Double) {
        knownObjects.applyChanges(cycleTransaction)
        cycleTransaction = Transaction()
        knownObjects.forEach { cycleTransaction.accumulate(it.tick(deltaTime)) }
    }

And then: … No.

Start #3

Another false start. revert.

The creation of our cycleTransaction should be in cycle, as should the application thereof:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
    }

We’ll create a Transaction up here, and apply it up here. We’ll need to pass it to each of our called inner guys, but that should be quick. Unless this is another false start. Which is OK, each time we’re learning a way not to do things.

Aside
That’s not quite what we wind up doing. We wind up creating a new Transaction for each step in the cycle.

If we are going to create the Transaction and use it inside cycle, we don’t need the member variable. That should help us find all the places we need to change.

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        val cycleTransaction = Transaction()
        lastTime = seconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
        knownObjects.applyChanges(cycleTransaction)
    }

Now we’ll go about passing the transaction in to each of these guys.

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        val cycleTransaction = Transaction()
        lastTime = seconds
        tick(deltaTime, cycleTransaction)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
        knownObjects.applyChanges(cycleTransaction)
    }

We pass it in to just this one guy, who turns, at first, into this one line function:

    fun tick(deltaTime: Double, trans: Transaction) {
        knownObjects.forEach { trans.accumulate(it.tick(deltaTime)) }
    }

I’m not calling the false start yet, but I think this may get messy. Let’s push a bit further and see. The issue is that everything is broken now because all the other methods have no transaction to refer to. Let’s follow our nose. This code, so far, should work. It’s the ones that can’t find the transaction to accumulate into who are unhappy, such as:

    fun processInteractions() {
        val toBeRemoved = colliders()
        if ( toBeRemoved.size > 0 ) {
            knownObjects.removeAndFinalizeAll(toBeRemoved)
        }
    }

Start #4

OK, I’m calling a false start again. Five yard penalty, repeat of down. I want to think more clearly about when we actually apply our transactions to our known objects. What we see above in processInteractions, we apply the results of the interactions immediately after the interactions are complete. And then we call this:

    private fun finishInteractions() {
        val buffer = Transaction()
        knownObjects.forEach {
            val result: Transaction = it.finishInteraction()
            buffer.accumulate(result)
        }
        knownObjects.applyChanges(buffer)
    }

We inform all the objects that interaction is finished and again we apply all the changes that come back. So why is that member variable up there being used in tick?

    fun tick(deltaTime: Double) {
        knownObjects.applyChanges(cumulativeTransactionFromUpdates)
        cumulativeTransactionFromUpdates.clear()
        knownObjects.forEach { cumulativeTransactionFromUpdates.accumulate(it.tick(deltaTime)) }
    }

This one method is saving transactions over the whole cycle. Follow what happens starting from the first tick:

  1. cumulative is empty, no changes made;
  2. cumulative is cleared for good measure;
  3. We tick everyone, saving their changes in cumulative;
  4. All the other events happen, updating as they go:
  5. We apply the results of last season’s tick.

Before we do anything else, I want to try applying tick immediately.

    fun tick(deltaTime: Double) {
        var trans = Transaction()
        knownObjects.forEach { trans.accumulate(it.tick(deltaTime)) }
        knownObjects.applyChanges(trans)
    }

Let’s see if this breaks tests or what … tests and game work. Let’s push that transaction down into the implementors of tick. I’ll just pass it here, and then deal with the upshot.

    fun tick(deltaTime: Double) {
        var trans = Transaction()
        knownObjects.forEach { it.tick(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

IDEA is upset now because it knows that tick doesn’t like that. We’ll just go into the implementors and fix them up. How many could there be? Oh, wow, IDEA wants to help by adding the parameter to tick. Let’s let it do that.

It changed this:

class SpaceObject
    fun tick(deltaTime: Double, trans: Transaction): Transaction {
        elapsedTime += deltaTime
        return update(deltaTime)
    }

This is one of the concrete methods that Hill objects to. We’re suppose to return a transaction, let’s change that. We can do this locally:

    fun tick(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        trans.accumulate( update(deltaTime))
    }

All should be good. Test. Game works, some tests want revision:

    fun `Asteroids Exist and Move`() {
        val asteroid = SolidObject.asteroid(
            pos = Point.ZERO,
            vel = Velocity(15.0,30.0)
        )
        asteroid.tick(tick*60, trans)
        checkVector(asteroid.position, Point(15.0, 30.0),"asteroid position")
    }

It appears that when we changed the signature, IDEA puts in a variable to fill in the space but that won’t compile. Interesting. Anyway here:

        asteroid.tick(tick*60, Transaction())

This test needs a bit more work:

    @Test
    fun `ShipChecker does nothing if ship seen`() {
        val ship = SolidObject.ship(U.randomPoint())
        val checker = ShipChecker(ship)
        checker.beginInteraction()
        val nothing = checker.interactWith(ship)
        assertThat(nothing).isEmpty()
        val emptyTransaction = checker.finishInteraction()
        assertThat(emptyTransaction.adds).isEmpty()
        assertThat(emptyTransaction.removes).isEmpty()
        val alsoNothing = checker.tick(0.01, trans)
        assertThat(alsoNothing.adds).isEmpty()
        assertThat(alsoNothing.removes).isEmpty()
    }
    @Test
    fun `ShipChecker does nothing if ship seen via withOther`() {
        val ship = SolidObject.ship(U.randomPoint())
        val checker = ShipChecker(ship)
        checker.beginInteraction()
        val nothing = checker.interactWithOther(ship)
        assertThat(nothing).isEmpty()
        val emptyTransaction = checker.finishInteraction()
        assertThat(emptyTransaction.adds).isEmpty()
        assertThat(emptyTransaction.removes).isEmpty()
        val alsoNothing = Transaction()
        checker.tick(0.01, alsoNothing)
        assertThat(alsoNothing.adds).isEmpty()
        assertThat(alsoNothing.removes).isEmpty()
    }

I have been interrupted for breakfast, our Sunday morning TV program, and a somewhat heated exchange with Hill. Back to making my tests run. I’ll report any interesting changes but so far they’re al. just accommodating the fact that tick updates a Transaction instead of returning one.

No surprises, but lots of changes. A bit tedious. Can now Commit: tick methods update a Transaction rather than returning one.

Now can we push the transaction further down?

In SpaceObject:

    fun tick(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        trans.accumulate( update(deltaTime))
    }

We wish to push trans down to update, which is defined in the SpaceObject:

    open fun update(deltaTime: Double): Transaction { return Transaction() }

We were here before. Let’s try changing the signature again.

    open fun update(deltaTime: Double, trans: Transaction) { }

Find and fix implementors. SolidObject has:

    override fun update(deltaTime: Double, trans: Transaction) {
        return controls.control(this, deltaTime).also { move(deltaTime) }
    }

We are invited to push trans down, or to update trans here and move sideways. Let’s try moving down this time:

    override fun update(deltaTime: Double, trans: Transaction) {
        controls.control(this, deltaTime, trans)
        move(deltaTime)
    }

This isn’t happy because of this:

class Controls
    fun control(ship: SolidObject, deltaTime: Double): Transaction {
        if (hyperspace) {
            hyperspace = false
            recentHyperspace = true
            return Transaction().also{ it.addAll(listOf(SolidObject.shipDestroyer(ship))) }
        }
        turn(ship, deltaTime)
        accelerate(ship, deltaTime)
        return Transaction().also { it.addAll(fire(ship)) }
    }

We want this to update a provided transaction, not return one, so:

    fun control(ship: SolidObject, deltaTime: Double, trans: Transaction) {
        if (hyperspace) {
            hyperspace = false
            recentHyperspace = true
            trans.addAll(listOf(SolidObject.shipDestroyer(ship)))
        }
        turn(ship, deltaTime)
        accelerate(ship, deltaTime)
        trans.addAll(fire(ship))
    }

I expect that to be OK but unfortunately I have another implementor to deal with, in WaveMaker:

    override fun update(deltaTime: Double, trans: Transaction) {
        if (elapsedTime < 3.0) return Transaction()

        val toAdd = mutableListOf<SpaceObject>()
        for (i in 1..numberToCreate) {
            val a = SolidObject.asteroid((U.randomEdgePoint()))
            toAdd.add(a)
        }
        return Transaction().also {
            it.addAll(toAdd)
            it.remove(this)
        }
    }

OK …

    override fun update(deltaTime: Double, trans: Transaction) {
        if (elapsedTime < 3.0) return

        val toAdd = mutableListOf<SpaceObject>()
        for (i in 1..numberToCreate) {
            val a = SolidObject.asteroid((U.randomEdgePoint()))
            toAdd.add(a)
        }
        trans.addAll(toAdd)
        trans.remove(this)
    }

Oops, I needed this:

    fun tick(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        update(deltaTime,trans)
    }

We are green. Commit: tick and update now accept accumulating Transaction rather than returning one.

What’s next? I think this:

class SpaceObject
    open fun finishInteraction(): Transaction = Transaction()

This wants this signature:

    open fun finishInteraction(trans: Transaction) = Transaction()

Find implementors:

class ShipChecker
    override fun finishInteraction(trans: Transaction) {
        val trans = Transaction()
        if ( missingShip ) {
            trans.add(ShipMaker(ship))
            trans.remove(this)
        }
        return trans
    }
    override fun finishInteraction(trans: Transaction) {
        if ( missingShip ) {
            trans.add(ShipMaker(ship))
            trans.remove(this)
        }
    }

I wish I could test these one at a time. Must think about how to do this more incrementally.

class ShipMaker
    override fun finishInteraction(trans: Transaction) {
        return if (elapsedTime > U.MAKER_DELAY && safeToEmerge) {
            replaceTheShip()
        } else {
            Transaction()
        }
    }

Here we’ll pass the trans to replace and don’t need the else.

    private fun replaceTheShip(trans: Transaction) {
        trans.add(ship)
        trans.add(ShipChecker(ship))
        trans.remove(this)
        trans.accumulate(Transaction.hyperspaceEmergence(ship,asteroidTally))
    }

Here we have that odd helper on Transaction. Let’s make a lot of that but leave it for how.

class WaveChecker
    override fun finishInteraction(trans: Transaction) {
        if ( elapsedTime > 1.0  ) {
            elapsedTime = 0.0
            if (!sawAsteroid) {
                elapsedTime = -5.0 // judicious delay to allow time for creation
                return Transaction().also {it.add(WaveMaker(4))}
            }
        }
        return Transaction()
    }

That becomes …

    override fun finishInteraction(trans: Transaction) {
        if ( elapsedTime > 1.0  ) {
            elapsedTime = 0.0
            if (!sawAsteroid) {
                elapsedTime = -5.0 // judicious delay to allow time for creation
                trans.add(WaveMaker(4))}
            }
        }
    }

I think we should be green again. Not quite:

    override fun finishInteraction(trans: Transaction) {
        if (elapsedTime > U.MAKER_DELAY && safeToEmerge) {
            replaceTheShip(trans)
        }
    }

Oops again, I needed to change this in Game:

    private fun finishInteractions() {
        val buffer = Transaction()
        knownObjects.forEach {
            it.finishInteraction(buffer)
        }
        knownObjects.applyChanges(buffer)
    }

Some tests are unhappy. Same issues as before, they all just want transactions poked in.

After more silly fiddling, green. Commit: finishInteractions takes accumulating transaction parameter rather than returning one.

Now, as we look at Game.cycle, we have questions to answer:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        draw(drawer)
    }

When should we update the game’s objects? Currently, we do it severally:

    fun tick(deltaTime: Double) {
        val trans = Transaction()
        knownObjects.forEach { it.tick(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

    private fun finishInteractions() {
        val buffer = Transaction()
        knownObjects.forEach {
            it.finishInteraction(buffer)
        }
        knownObjects.applyChanges(buffer)
    }

The processInteractions function doesn’t use Transaction, instead returning a list of objects, but it probably should be converted to the new scheme. The update is done in there as well, however:

    fun processInteractions() {
        val toBeRemoved = colliders()
        if ( toBeRemoved.size > 0 ) {
            knownObjects.removeAndFinalizeAll(toBeRemoved)
        }
    }

It seems that the rule is that there are some processes that run “for all objects” and that when each of those is done, we apply the results before going to the next process. It need not be that way: we could imagine that we accumulate all the transactions for an entire cycle and then apply them. I kind of suspect that was what tick was thinking we’d do with the cumulativeTransactionsWhatever object.

Which is correct? Either / both? I think that in principle, updating as soon as possible is desirable. Since we cannot update while we are looping the known objects, updating right after we’re done makes some sense. There might be a more transparent way to do it, but I don’t see it just now.

I think this is enough changing for now.

In another part of the forest, to borrow his phrase, Hill has been making some changes that I think are good (and some that I consider odious), but his version has diverged so much from mine that I probably can’t pull them, even if I were the sort of person who deals with pull requests and even if he were the sort of person to issue them. So I’ll “just” have to make similar changes in due time.

One such change is that he has made who interacts with whom more explicit, by adding a flag that indicates whether an object really wants to lead in the dance of interaction or whether it doesn’t care. That change plus a check in the collision code allows sending the message to objects that prefer to lead without a lot of hassle. As such, it’s worth doing. He has changed the hierarchy of SpaceObjects, however, which makes adopting the idea much harder.

If one were interested in grand lessons, the lesson here would be to avoid long-lived branches. But that is not exactly our mission here, which is something more like “can Hill come up with a design that avoids my sins while still remaining acceptable to my sadly mistaken notions of design”. This means he needs to be free to change anything, even things I would fight to leave alone.

So be it. I’m learning things and sometimes he seems to be enjoying himself so as the song says, let it be.

For now …

Summary

Today’s work involved no fewer than three false starts as I looked for a seam where I could make changes without too much cascading. In the final run, the changes to the code were quick, usually just changing two or three methods to match the new calling sequence. But the tests … oh my. There are lots of them, and they were not using the convention of passing a Transaction, instead relying on a return that we were removing. I briefly considered allowing the methods to return their transaction, for convenience in testing, but that seemed wrong. So I had to do quite a few repetitive trivial changes to the tests. Oh well.

We’ve moved in the direction of accumulating changes into Transactions, passing them down to functions that previously would have created them. I wanted to try this idea, because it seemed to me to have merit and it seems to produce somewhat simpler code. Passing around an accumulating variable is a time-honored idea, so it’s probably OK. It’s working, and the code is smaller, so it must be good, right? Right? Bueller?

See you next time!



  1. Jeffries and his French phrases. What’s wrong with plain Amurican? 

  2. Oh, stop!