Two ways of doing a thing appeared on the Codea forum today. I’d like to see whether we can transform the one into the other in small steps.

Today, the following program was posted on the Codea forum, by a fairly new member, showing one way to display a typed message one character at a time:

-- Typewriter

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    viewer.mode = FULLSCREEN
end

function initiate()

end
-- Initiate typewriting action!
function typeWriter(t, x)
    font("Courier-Bold")
    fontSize(25)
    text(t, x, WIDTH/2 - 300)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    nOS = 1
    if ElapsedTime > nOS then
        typeWriter("H", WIDTH/2)
        nOS = 1.1
    end
    if ElapsedTime > nOS then
        typeWriter("e", WIDTH/2 + 15)
        nOS = 1.2
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 30)
        nOS = 1.3
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 45)
        nOS = 1.4
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 60)
        nOS = 1.5
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 75)
        nOS = 1.5
    end

    if ElapsedTime > nOS then
        typeWriter("G", WIDTH/2 + 105)
        nOS = 1.6
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 120)
        nOS = 1.7
    end

    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 150)
        nOS = 1.8
    end
    if ElapsedTime > nOS then
        typeWriter("w", WIDTH/2 + 165)
        nOS = 1.9
    end
    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 180)
        nOS = 2
    end
    if ElapsedTime > nOS then
        typeWriter("y", WIDTH/2 + 195)
        nOS = 2.1
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 210)
        nOS = 2.2
    end
end

The ever-helpful Dave1707 responded with this example:

viewer.mode=FULLSCREEN

function setup()
    textMode(CORNER)
    fill(255)
    fontSize(40)
    h=0
    cnt=1
    str="abcdefghijklmnopqrstuvwxyz 1234567890"
    str1=""
end

function draw()
    background()
    e=ElapsedTime
    if cnt<=#str then
        if e>h+1 then
            str1=string.sub(str,1,cnt)
            cnt=cnt+1        
            h=e
        end
    end
    text(str1,100,HEIGHT/2)
end

Now, there’s probably little dispute that the second program is “better” than the first, although one might argue that it is far less clear how it works. The original program is, I would say without prejudice, kind of a “beginner’s” program, as might be written by someone not yet very experienced in programming. We were all there at one point, and I don’t want to show you my first program at all.

But to go from being able to write the first program, to being able to write the second one … that’s a very big leap in learning and practice. So I’m wondering … can we transform the original program to the second one, bit by bit.

I’m going to try. Here goes. I’ve created a copy in my Codea of the first program. My first step is to try to understand how it works.

Overall, we see it comparing ElapsedTime with nOS, and when ElapsedTime is larger, calling typewriter with a character and an x coordinate, starting at WIDTH/2 and incrementing by 15.

The various if statements each type one character, each time a bit further over in x, and each time with a larger value of nOS being checked. The starting value of nOS is 1. So the draw, which runs many times per second, every time through, it sets nOS first to 1 and then checks to see if ElapsedTime is larger than that and if so, types H, and then sets nOS to be 1.1, and checks that, and so on, down to the end of the list of characters to be typed.

Since ElapsedTime increases without bound, once it attains a value of 2.1, the highest value checked for, thereafter, all the ifs will be true and the whole string will type out.

Here’s a video of the program running.

hello. go away

Now, frequent readers will know that I am all about removing duplication, which is so often a sign that something can be improved. It’s also very easy to spot, and in this program we see lots of duplication.

But sometimes we need to make the duplication worse before we can make it better. We notice that in each step in the program, we add 0.1 to the value of nOS. So we can refactor the program this way:

    nOS = 1
    if ElapsedTime > nOS then
        typeWriter("H", WIDTH/2)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("e", WIDTH/2 + 15)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 30)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 45)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 60)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 75)
        nOS = nOS + 0.1
    end
    
    if ElapsedTime > nOS then
        typeWriter("G", WIDTH/2 + 105)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 120)
        nOS = nOS + 0.1
    end
    
    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 150)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("w", WIDTH/2 + 165)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 180)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter("y", WIDTH/2 + 195)
        nOS = nOS + 0.1
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 210)
        nOS = nOS + 0.1
    end

That works just the same. If we had tests, we could be more sure of that. Maybe we’ll do that next time. Today we’re just going to try to move very safely forward, running the program to be sure we haven’t broken it.

Now when I started out with this idea, I was thinking that I could “just” pull out the common line and move it to the bottom of the draw, but I see now that that’s not quite the case. Let’s try it and find out why not.

When we do that change, there’s a one second delay, but then all the text comes out at once, as far as the eye can see:

hello fast

So that won’t do. I can see a couple of things that might work. This one is simple. In the original program, we update nOS only if we call typewriter. Since it happens that nOS is a global, we can do that:

function typeWriter(t, x)
    font("Courier-Bold")
    fontSize(25)
    text(t, x, WIDTH/2 - 300)
    nOS = nOS + 0.1
end

After removing all the inner increments, the program works as before. I’ll spare you the video, but here’s the program:


-- Typewriter

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    viewer.mode = FULLSCREEN
end

function initiate()
    
end

-- Initiate typewriting action!

function typeWriter(t, x)
    font("Courier-Bold")
    fontSize(25)
    text(t, x, WIDTH/2 - 300)
    nOS = nOS + 0.1
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    nOS = 1
    if ElapsedTime > nOS then
        typeWriter("H", WIDTH/2)
    end
    if ElapsedTime > nOS then
        typeWriter("e", WIDTH/2 + 15)
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 30)
    end
    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 45)
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 60)
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 75)
    end
    
    if ElapsedTime > nOS then
        typeWriter("G", WIDTH/2 + 105)
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 120)
    end
    
    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 150)
    end
    if ElapsedTime > nOS then
        typeWriter("w", WIDTH/2 + 165)
    end
    if ElapsedTime > nOS then
        typeWriter("a", WIDTH/2 + 180)
    end
    if ElapsedTime > nOS then
        typeWriter("y", WIDTH/2 + 195)
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 210)
    end
    
end

Works just the same. And it’s rather a lot shorter, which is a good thing. I believe that if I were pairing with the original author, I could explain what we’re doing and they’d likely see that they could do it. I might even have them doing the typing if that seemed helpful.

Now there’s that init of nOS to 1, which is done inside the draw. I suspect that can be moved up to the setup. I’m not prepared to reason about it, but I’m prepared to try it.

It turns out that I’m wrong. When I do that, the program just kind of types H in a blurry on/off fashion. Doesn’t do the rest of the job. Irritating. We’ll try something else. I’m not sure why that didn’t work, but we’re trying to avoid complicated “programmer” reasoning here, because we’re pretending to be helping a beginner make their program better, not trying to be a big serious programmer.

And I like to avoid complicated reasoning anyway, it only gets me in trouble. So let’s see what else we might be able to improve.

We notice that the X value increases by 15 on each letter. Let’s do what we did before, create some extra duplication and then remove it.


    nOS = 1
    X = WIDTH/2
    if ElapsedTime > nOS then
        typeWriter("H", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("e", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("l", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("l", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("o", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter(".", X)
        X = X + 15
    end
    
    if ElapsedTime > nOS then
        typeWriter("G", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("o", X)
        X = X + 15
    end
    
    if ElapsedTime > nOS then
        typeWriter("a", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("w", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("a", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter("y", X)
        X = X + 15
    end
    if ElapsedTime > nOS then
        typeWriter(".", X)
        X = X + 15
    end

This, amusingly, nearly works!

no space

There’s no space between “Go” and “away”! I didn’t notice that in the original program, the programmer stepped by 30 that one time instead of 15.

What shall we do? We talk about it and I propose putting in another if, with a space to display:

    if ElapsedTime > nOS then
        typeWriter("o", X)
        X = X + 15
    end
    
    if ElapsedTime > nOS then
        typeWriter(" ", X)
        X = X + 15
    end
    
    if ElapsedTime > nOS then
        typeWriter("a", X)
        X = X + 15
    end

We agree that it’s worth trying and sure enough, it works.

This problem makes us go back and look at the original program, where we find this:

    if ElapsedTime > nOS then
        typeWriter("l", WIDTH/2 + 45)
        nOS = 1.4
    end
    if ElapsedTime > nOS then
        typeWriter("o", WIDTH/2 + 60)
        nOS = 1.5
    end
    if ElapsedTime > nOS then
        typeWriter(".", WIDTH/2 + 75)
        nOS = 1.5
    end

    if ElapsedTime > nOS then
        typeWriter("G", WIDTH/2 + 105)
        nOS = 1.6
    end

There are two 1.5s in there that I didn’t notice. I ask the original programmer whether they actually intended to type the o and the dot at the same time, and no, they agree that that was a typo. So we think our adding 0.1 is consistent with what we want throughout.

So now we have all those duplicated X = X + 15 lines and we move that value up into typeWriter as well.

Along the way we notice the other missing space, and we wind up with this:


-- Typewriter

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    viewer.mode = FULLSCREEN
end

function initiate()
    
end

-- Initiate typewriting action!

function typeWriter(t, x)
    font("Courier-Bold")
    fontSize(25)
    text(t, x, WIDTH/2 - 300)
    nOS = nOS + 0.1
    X = X + 15
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    nOS = 1
    X = WIDTH/2
    if ElapsedTime > nOS then
        typeWriter("H", X)
    end
    if ElapsedTime > nOS then
        typeWriter("e", X)
    end
    if ElapsedTime > nOS then
        typeWriter("l", X)
    end
    if ElapsedTime > nOS then
        typeWriter("l", X)
    end
    if ElapsedTime > nOS then
        typeWriter("o", X)
    end
    if ElapsedTime > nOS then
        typeWriter(".", X)
    end
    
    if ElapsedTime > nOS then
        typeWriter(" ", X)
    end
    
    if ElapsedTime > nOS then
        typeWriter("G", X)
    end
    if ElapsedTime > nOS then
        typeWriter("o", X)
    end
    
    if ElapsedTime > nOS then
        typeWriter(" ", X)
    end
    
    if ElapsedTime > nOS then
        typeWriter("a", X)
    end
    if ElapsedTime > nOS then
        typeWriter("w", X)
    end
    if ElapsedTime > nOS then
        typeWriter("a", X)
    end
    if ElapsedTime > nOS then
        typeWriter("y", X)
    end
    if ElapsedTime > nOS then
        typeWriter(".", X)
    end
    
end

Now what? We have this duplication of the if statement. Surely we can just move that up inside typeWriter, like this, after a bit of reformatting and comment removal:

-- Typewriter

function setup()
    print("Hello World!")
    viewer.mode = FULLSCREEN
end

function initiate()
    
end

-- Initiate typewriting action!

function typeWriter(t, x)
    if ElapsedTime > nOS then
        font("Courier-Bold")
        fontSize(25)
        text(t, x, WIDTH/2 - 300)
        nOS = nOS + 0.1
        X = X + 15
    end
end

function draw()
    background(40, 40, 50)
    strokeWidth(5)
    
    nOS = 1
    X = WIDTH/2
    typeWriter("H", X)
    typeWriter("e", X)
    typeWriter("l", X)
    typeWriter("l", X)
    typeWriter("o", X)
    typeWriter(".", X)
    
    typeWriter(" ", X)
    
    typeWriter("G", X)
    typeWriter("o", X)
    
    typeWriter(" ", X)
    
    typeWriter("a", X)
    typeWriter("w", X)
    typeWriter("a", X)
    typeWriter("y", X)
    typeWriter(".", X)
    
end

This continues to work as intended. We’ve fixed a few misunderstandings along the way, regarding the spacing, which was done by setting special values, where we now just type a space.

We notice that the parameter X isn’t much use down in draw so we remove it:

function typeWriter(t)
    if ElapsedTime > nOS then
        font("Courier-Bold")
        fontSize(25)
        text(t, X, WIDTH/2 - 300)
        nOS = nOS + 0.1
        X = X + 15
    end
end

-- This function gets called once every frame
function draw()
    background(40, 40, 50)
    strokeWidth(5)
    
    nOS = 1
    X = WIDTH/2
    typeWriter("H")
    typeWriter("e")
    typeWriter("l")
    typeWriter("l")
    typeWriter("o")
    typeWriter(".")
    
    typeWriter(" ")
    
    typeWriter("G")
    typeWriter("o")
    
    typeWriter(" ")
    
    typeWriter("a")
    typeWriter("w")
    typeWriter("a")
    typeWriter("y")
    typeWriter(".")
    
end

I just noticed that function initiate which has nothing in it. Let’s use it to put our various initialization code in there, like this:


function initiate()
    background(40, 40, 50)
    strokeWidth(5)
    
    nOS = 1
    X = WIDTH/2
end

-- Initiate typewriting action!

function typeWriter(t)
    if ElapsedTime > nOS then
        font("Courier-Bold")
        fontSize(25)
        text(t, X, WIDTH/2 - 300)
        nOS = nOS + 0.1
        X = X + 15
    end
end

-- This function gets called once every frame
function draw()
    initiate()
    typeWriter("H")
    typeWriter("e")
    typeWriter("l")
    typeWriter("l")
    typeWriter("o")
    typeWriter(".")
    
    typeWriter(" ")
    
    typeWriter("G")
    typeWriter("o")
    
    typeWriter(" ")
    
    typeWriter("a")
    typeWriter("w")
    typeWriter("a")
    typeWriter("y")
    typeWriter(".")
    
end

So that’s nice. We can also move the boilerplate from typeWriter into initiate:

function initiate()
    background(40, 40, 50)
    strokeWidth(5)
    font("Courier-Bold")
    fontSize(25)
    
    nOS = 1
    X = WIDTH/2
end

function typeWriter(t)
    if ElapsedTime > nOS then
        text(t, X, WIDTH/2 - 300)
        nOS = nOS + 0.1
        X = X + 15
    end
end

Everything all works just fine. The drawing function still has some duplication in it. It looks like this:

function draw()
    initiate()
    typeWriter("H")
    typeWriter("e")
    typeWriter("l")
    typeWriter("l")
    typeWriter("o")
    typeWriter(".")
    
    typeWriter(" ")
    
    typeWriter("G")
    typeWriter("o")
    
    typeWriter(" ")
    
    typeWriter("a")
    typeWriter("w")
    typeWriter("a")
    typeWriter("y")
    typeWriter(".")
    
end

Is there perhaps some way we could eliminate that duplication? I think there might be. Could we introduce a loop? We might have to explain the idea to our pair, depending on how experienced they are. If they are a rank beginner, we might do some separate examples. Be that as it may, we might come down to something like this:

function draw()
    initiate()
    local s = "Hello. Go away."
    for i = 1, #s do
        typeWriter(s:sub(i,i))
    end
end

This is a bit of a big step, if you don’t know Lua and programming very well yet. We’d have to develop the notion of a loop, and the notion that Lua can fetch a character from a string using string:sub(i, i). We’d do that with some simple examples, I imagine. I think I’d like to move the string definition upward. We’d probably like to be able to type anything at all. So we might do something like this:

function typeString(str)
    for i = 1, #str do
        typeWriter(str:sub(i,i))
    end
end

function draw()
    initiate()
    typeString("Hello. Go away.")
end

Here’s the entire program as it now stands:


-- Typewriter

function setup()
    viewer.mode = FULLSCREEN
end

function initiate()
    background(40, 40, 50)
    strokeWidth(5)
    font("Courier-Bold")
    fontSize(25)
    
    nOS = 1
    X = WIDTH/2
end

function typeWriter(t)
    if ElapsedTime > nOS then
        text(t, X, WIDTH/2 - 300)
        nOS = nOS + 0.1
        X = X + 15
    end
end

function typeString(str)
    for i = 1, #str do
        typeWriter(str:sub(i,i))
    end
end

function draw()
    initiate()
    typeString("Hello. Go away.")
end

That’s 30 lines, including blank lines. Dave’s program is 23 lines if I’ve counted correctly. Thinking of the transition from the first version to either this one or Dave’s, it looks a lot like “And then a miracle occurs”. I’m pretty sure Dave just figured out what the problem was and coded it up. Here, we took a different path, which was to start with a working program and slowly modify it to make it “better”, by our standards for “better”. I’m not prepared to compare the two “final” versions to see which one is better. There’s no objective way to do that, and I know that Dave’s preferred style and mine are different. And that’s OK.

There are still things that I don’t like about the program that we have here in hand. Let’s see …

  1. We have two values that both increment as time ticks, nOS and X.
  2. We’re hard-wired to where the text displays.
  3. There’s something I don’t like about the fact that we always try to type all the characters even when nOS isn’t going to let them come out. Dave’s program is a bit better there, if you figure it out: he only moves characters into the output when they’re due to print.
  4. We’re displaying the characters one at a time, with separate text calls. That’s arguably inefficient. Here again, Dave’s program is better.

Let’s see what we can do about those concerns.

First, I want to reason about the two values nOS and X. In particular, I want to relate them to the variable i, the index of the character we’re going to display.

A bit of scribbling will tell us that when i has the value n, then nOS will be 1 + (n-1)*0.1 and X will be WIDTH/2 + (n-1)*15. Let’s pass in i, and compute those values directly.

function typeWriter(char, n)
    nOS = 1 + (n-1)*0.1
    if ElapsedTime > nOS then
        X = WIDTH/2 + (n-1)*15
        text(char, X, WIDTH/2 - 300)
    end
end

function typeString(str)
    for i = 1, #str do
        typeWriter(str:sub(i,i),i)
    end
end

Now it turns out that nOS and X can be local:

function typeWriter(char, n)
    local nOS = 1 + (n-1)*0.1
    if ElapsedTime > nOS then
        local X = WIDTH/2 + (n-1)*15
        text(char, X, WIDTH/2 - 300)
    end
end

So that’s nice. I think those variable names might be better. Lower case X is better for a local:

function typeWriter(char, n)
    local nOS = 1 + (n-1)*0.1
    if ElapsedTime > nOS then
        local x = WIDTH/2 + (n-1)*15
        text(char, x, WIDTH/2 - 300)
    end
end

And what is nOS anyway? Well, it’s an elapsed time, and if it equals T >= 1, then we should display characters T through T + (T-1)*10. That is:

T # to display
1.0 1
1.1 2
1.3 3

Now we dare not just do that with arithmetic, because we know that adding up tenths doesn’t work well on computers. We might do a little experiment with our pair if they’re not familiar with the risks of binary-decimal conversion.

What if we incremented nOS by 1 instead of 0.1 and divided it back out to get the time? Something like this:

function typeWriter(char, n)
    local nOS = 10 + (n-1)
    if ElapsedTime > nOS/10 then
        local x = WIDTH/2 + (n-1)*15
        text(char, x, WIDTH/2 - 300)
    end
end

Hm, that’s not making me any happier. Put that back.

What if we rename nOS? Let’s call it timeToDelayForCharacterN.

function typeWriter(char, n)
    local timeToDelayForCharacterN = 1 + (n-1)*0.1
    if ElapsedTime > timeToDelayForCharacterN then
        local x = WIDTH/2 + (n-1)*15
        text(char, x, WIDTH/2 - 300)
    end
end

Kind of long, but calls it as we see it. Let’s change the x similarly.

function typeWriter(char, n)
    local timeToDelayForCharacterN = 1 + (n-1)*0.1
    if ElapsedTime > timeToDelayForCharacterN then
        local xPositionForCharacterN = WIDTH/2 + (n-1)*15
        text(char, xPositionForCharacterN, WIDTH/2 - 300)
    end
end

YMMV, but I think when I come back to this program in a year or so, I’m going to prefer the longer names and they certainly aren’t hurting anyone.

Of the things on my list of concerns above, the one I’d really like to fix is the one about calling text multiple times. Now the truth is, we won’t be able to measure the difference between the two cases, but still it might be seen as better to call text only once. But how can we do it?

I guess every time we call typeWriter, we want to add one more character to the output, if that character’s time is up. And after we’re done calling typeWriter, only then do we want to do a single text command to display the string.

So we need a place to save the accumulating string. Let’s start with a global to make it work, and then see what we can do that’s better. I have a fairly good feeling about this.

Our first attempt is this:

function initiate()
    background(40, 40, 50)
    strokeWidth(5)
    font("Courier-Bold")
    fontSize(25)
    textMode(LEFT)
    output_string = ""
end

function typeWriter(char, n)
    local timeToDelayForCharacterN = 1 + (n-1)*0.1
    if ElapsedTime > timeToDelayForCharacterN then
        output_string = output_string .. char
    end
end

function typeString(str)
    for i = 1, #str do
        typeWriter(str:sub(i,i),i)
    end
end

function draw()
    initiate()
    typeString("Hello. Go away.")
    text(output_string, WIDTH/2, WIDTH/2 - 300)
end

Note that we had to add textMode(LEFT), since the default is CENTER. Now our code accumulates into output_string and then just texts it out once, at WIDTH/2. So we didn’t need that x value any more.

Now the irritating global. What if typeString could return the value accumulated?

We could write this:

function draw()
    initiate()
    local t = typeString("Hello. Go away.")
    text(t, WIDTH/2, WIDTH/2 - 300)
end

That’s relatively sweet, so let’s make it work:

function typeWriter(char, n, output)
    local timeToDelayForCharacterN = 1 + (n-1)*0.1
    if ElapsedTime > timeToDelayForCharacterN then
        output = output .. char
    end
    return output
end

function typeString(str)
    local output = ""
    for i = 1, #str do
        output = typeWriter(str:sub(i,i),i, output)
    end
    return output
end

We pass an empty string into typeWriter and add our character to it if it’s time for that character. Either way, we return either the longer string or the original.

When we’ve tried all the characters, we return the string to the typeString caller and he can text it wherever he wants.

I think that’ll do for now. Let’s sum up.

Summary

Here’s the whole program as it stands:

-- Typewriter

function setup()
    viewer.mode = FULLSCREEN
end

function initiate()
    background(40, 40, 50)
    strokeWidth(5)
    font("Courier-Bold")
    fontSize(25)
    textMode(LEFT)
end

function typeWriter(char, n, output)
    local timeToDelayForCharacterN = 1 + (n-1)*0.1
    if ElapsedTime > timeToDelayForCharacterN then
        output = output .. char
    end
    return output
end

function typeString(str)
    local output = ""
    for i = 1, #str do
        output = typeWriter(str:sub(i,i),i, output)
    end
    return output
end

function draw()
    initiate()
    local t = typeString("Hello. Go away.")
    text(t, WIDTH/2, WIDTH/2 - 300)
end

It seems to behave just like the original. I’d bet, if I had to, that it’s about 7.5 pixels to the right of where it was, owing to the difference between LEFT and CENTER. Call me a liar for that, will you?

The program now has no global variables, and it doesn’t increment any strange values, though it does calculate the interesting timeToDelayForCharacterN, which could be parameterized or something. Either way, I’d rather compute it directly than have it accumulating.

We do accumulate the string, which was an explicit choice to avoid multiple calls to text, mostly just to see if we could.

Lesson?

If there is a lesson to be had here, I think it is roughly this:

We’ve reduced this program from about 84 lines to 36, and done so in very small steps, each of which we could justify for a specific short-term need, and which we could explain–fairly readily–to a beginner programmer.

We didn’t get quite to Dave’s version, nor should we expect to. But what we might be learning is that even from a very weak starting point, we can slowly refine a program and bring it into decent alignment with our best understanding of what makes good code.

That’ll do for today!

See you again soon!