Kotlin 171: A Better Another Idea
In which, our intrepid hero has yet another idea. Will this one be better, or just another idea?
I rather like the tellMeWhen
object. It takes advantage of our poor-man’s multi-tasking game cycle to set up an event for “later”. But in use, it’s not as clear as it might be:
class WaveChecker: ISpaceObject, InteractingSpaceObject {
private var asteroidsMissing = true
var makingWave = false
var numberToCreate = 4
override fun update(deltaTime: Double, trans: Transaction) {}
override fun callOther(other: InteractingSpaceObject, trans: Transaction) = Unit
override val subscriptions = Subscriptions (
beforeInteractions = { asteroidsMissing = true},
interactWithAsteroid = { _, _ -> asteroidsMissing = false },
afterInteractions = this::makeWaveIfNeeded
)
private fun makeWaveIfNeeded(trans: Transaction) {
if ( asteroidsMissing && !makingWave ) {
makeWaveSoon(trans)
}
}
private fun makeWaveSoon(trans: Transaction) {
makingWave = true
TellMeWhen(4.0, trans) {
makeWave(it)
makingWave = false
}
}
private fun makeWave(trans: Transaction) {
for (i in 1..numberToCreate) {
trans.add(Asteroid(U.randomEdgePoint()))
}
}
}
And …
class SaucerMaker(saucer: Saucer = Saucer()): InteractingSpaceObject, ISpaceObject {
var saucerMissing: Boolean = true
var makingSaucer = false
override fun update(deltaTime: Double, trans: Transaction) {}
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {}
override val subscriptions: Subscriptions = Subscriptions(
beforeInteractions = { saucerMissing = true },
interactWithSaucer = { _, _ -> saucerMissing = false },
afterInteractions = { trans ->
if ( saucerMissing && !makingSaucer ) {
makingSaucer = true
TellMeWhen(7.0, trans) {
it.add(saucer)
makingSaucer = false
}
}
}
)
}
Now, even before I get to my idea, I am wondering why the WaveMaker seems so much more complicated than the SaucerMaker, even though at the bottom it seems they should be the same. What happened was that in the WaveMaker, I pulled out separate methods, hoping to make each step more clear. I’m not sure it’s really better. Let’s inline WaveMaker all the way back just to see what we get.
override val subscriptions = Subscriptions (
beforeInteractions = { asteroidsMissing = true},
interactWithAsteroid = { _, _ -> asteroidsMissing = false },
afterInteractions = { trans ->
if (this.asteroidsMissing && !this.makingWave) {
this.makingWave = true
TellMeWhen(4.0, trans) {
for (i in 1..this.numberToCreate) {
it.add(Asteroid(U.randomEdgePoint()))
}
this.makingWave = false
}
}
}
)
I hope we can agree that that’s too much nesting. Let’s drag out makeWave
again:
override val subscriptions = Subscriptions (
beforeInteractions = { asteroidsMissing = true},
interactWithAsteroid = { _, _ -> asteroidsMissing = false },
afterInteractions = { trans ->
if (asteroidsMissing && !makingWave) {
makingWave = true
TellMeWhen(4.0, trans) {
makeWave(it)
makingWave = false
}
}
}
)
private fun makeWave(it: Transaction) {
for (i in 1..this.numberToCreate) {
it.add(Asteroid(U.randomEdgePoint()))
}
}
Now we see the parallelism more clearly, but it’s still pretty weird. And the Land of the Weird is right where my idea belongs.
A thing that is happening in the afterInteractions
is that we have a flag we care about, asteroidsMissing
, and what we want to do is to create the new wave after a while, and we just want to do it once, because our afterTransactions
is going to be executed 60 times a second over that “while”, and we don’t want to create a new wave every time through.
All we really want is for makeWave(it)
to be called once and only once, passed a transaction so that it can do its thing. All that manipulation of the other flag makingWave
in the WaveChecker, and makingSaucer
in the SaucerMaker, is just there to do the “once and only once” idea.
My p-baked idea1 of the morning is to create an object that encapsulates “in X seconds, do this block once, and don’t do it again until it has completed”.
It’s going to be hard to name this baby. We’ll call it OneShot
for now
My basic idea on how it works is this:
- Create a
OneShot
, giving it a time delay and the block to be executed. The block need only include the code you actually want done, no creation of TellMeWhen, no special flags. - When you want the thing done (once and only once), you call the
OneShot
methodexecute(trans)
, passing it a live transaction.
Let’s just code it by intention. Our tests should tell us when we’ve got it right. I’ll do SaucerMaker, because it’s a bit simpler.
Here’s what I get:
class OneShot(private val delay: Double, private val action: (Transaction)->Unit) {
var triggered = false
fun execute(trans: Transaction) {
if (!triggered) {
triggered = true
TellMeWhen(delay, trans) {
triggered = false
action(it)
}
}
}
}
Shall I explain a bit? The class stores a time delay and an action to be done. It sets itself not triggered. When execute
is called, if it is not triggered, it notes that it has been triggered2, then creates a TellMeWhen, passing it the desired delay and a block that clears the triggered flag (when the delay is up) and that calls the provided action.
We do the action once, when not triggered, and can’t do it again until after the delayed action has been carried out.
I think we’ll want to rename TellMeWhen. Later. For now, here’s the OneShot in use:
class SaucerMaker(saucer: Saucer = Saucer()): InteractingSpaceObject, ISpaceObject {
private var oneShot = OneShot(7.0) { it.add(saucer) }
var saucerMissing: Boolean = true
override fun update(deltaTime: Double, trans: Transaction) {}
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {}
override val subscriptions: Subscriptions = Subscriptions(
beforeInteractions = { saucerMissing = true },
interactWithSaucer = { _, _ -> saucerMissing = false },
afterInteractions = { if ( saucerMissing ) oneShot.execute(it) }
)
}
I think that’s rather fine. Let’s try it in WaveChecker. (And shouldn’t we rename it WaveMaker?)
class WaveChecker: ISpaceObject, InteractingSpaceObject {
private val oneShot = OneShot(4.0) { makeWave(it) }
private var asteroidsMissing = true
var numberToCreate = 4
override fun update(deltaTime: Double, trans: Transaction) {}
override fun callOther(other: InteractingSpaceObject, trans: Transaction) = Unit
override val subscriptions = Subscriptions (
beforeInteractions = { asteroidsMissing = true},
interactWithAsteroid = { _, _ -> asteroidsMissing = false },
afterInteractions = { if (asteroidsMissing) oneShot.execute(it) }
)
private fun makeWave(it: Transaction) {
for (i in 1..this.numberToCreate) {
it.add(Asteroid(U.randomEdgePoint()))
}
}
}
Oh yes, I like this. I built OneShot inside SaucerMaker for convenience. Move it to its own file.
The only references to TellMeWhen are now in OneShot and in a few tests that look for it to be sure that our exchanges are working. I think it needs a better name.
While I’m thinking about it, let’s rename WaveChecker to WaveMaker. The name is available because we removed the old WaveMaker class yesterday.
IDEA is so good at that! It even renames the test from WaveCheckerTest to WaveMakerTest. Nice.
OK, what about TellMeWhen? What should its name be? Not terribly critical: it’s used only here, other than in tests:
fun execute(trans: Transaction) {
if (!triggered) {
triggered = true
TellMeWhen(delay, trans) {
triggered = false
action(it)
}
}
}
Should it be named DoLater?
fun execute(trans: Transaction) {
if (!triggered) {
triggered = true
DoLater(delay, trans) {
triggered = false
action(it)
}
}
}
DoInAWhile? Defer?
fun execute(trans: Transaction) {
if (!triggered) {
triggered = true
Defer(delay, trans) {
triggered = false
action(it)
}
}
}
WaitAndThen? I’m going to call it DeferredAction.
fun execute(trans: Transaction) {
if (!triggered) {
triggered = true
DeferredAction(delay, trans) {
triggered = false
action(it)
}
}
}
Done and done. Let’s sum up.
Summary
I noticed that to decide what to call the TellMeWhen, I tried it by pasting candidate names into actual code, to see how it worked when reading the code.
Even with the new class added, the total size of the source code has gone down by 5 lines, from 1026 to 1031. SaucerMaker went down eight lines, from 23 down to 15, WaveMaker lost ten, from 32 down to 22. So that’s good.
Coding objects like SaucerMaker and WaveMaker is now substantially simpler, because the one-time aspect we need is handled inside the OneShot and the user needs only to be concerned about the condition under which the thing should be done, asteroids missing, saucers missing, whatever.
I am well pleased with this outcome. The OneShot is a bit intricate, but it’s still only a few lines and you don’t have to think about how it works, only what it does. Encapsulation FTW.
Your thoughts are always welcome. See you next time!