I’m going to try to build a “fluid interface” feature for Codea’s graphical style capability. Why? I don’t know.

John, one of the creators of Codea, posted an idea for a future version. Instead of typing, say:

pushStyle()
fill(255)
stroke(255,0,0)
strokeWidth(5)

You could instead say:

style.push().fill(color.white).stroke(color.red).strokeWidth(5)

For reasons I truly cannot explain, I propose to try to implement this interface, mostly, I guess, because I’ve never done a “fluent interface” and wonder if there’s anything interesting or tricky to learn about.

I’ll begin, of course, with a test, in a new Project cleverly called “Style”. It’s not clear what to test first. I decide on this:

        _:test("Can set style", function()
            
        end)

Now I guess I want to type in a style setting sequence and test whether it gets set. How can I do that? The style-setting functions today are all globals. I don’t know. I’ll try this, assuming a variable in style, settings:

        _:test("Can set style", function()
            style.stroke(255).strokeWidth(5)
            local settings = style.settings
            _:expect(settings.stroke).is(255)
            _:expect(settings.strokeWidth).is(5)
        end)

This will fail with no idea what style is, I hope.

1: Can set style -- Tests:15: attempt to index a nil value (global 'style')

I love it when a plan comes together. Now let’s make it work.

Here’s my cut:

style = {}

function style.getSettings()
    if not style.settings then
        style.settings = {}
    end
    return style.settings
end

function style.stroke(arg)
    local s = style.getSettings()
    s.stroke = arg
    return style
end

function style.strokeWidth(arg)
    local s = style.getSettings()
    s.strokeWidth = arg
    return style
end

That works. We can improve that code:

style = {}

function style.getSettings()
    if not style.settings then
        style.settings = {}
    end
    return style.settings
end

function style.stroke(arg)
    style.getSettings().stroke = arg
    return style
end

function style.strokeWidth(arg)
    style.getSettings().strokeWidth = arg
    return style
end

Now there is an issue here, which is that we need to deal with the fact that, currently, a function like stroke, which takes a color, can be called with one, three, or four arguments, depending on whether you want a grayscale color, an RGB color, or an RGB with transparency. John was getting around that with his color.red call, which will be returning some kind of color “object”.

I think we’ll ignore that concern for now.

We have a more immediate issue, which is that this style thing isn’t actually setting the stroke or stroke width. Fortunately, the current calls all return the current setting if called with no arguments.

I suspect we’d better implement that directly in our functions. Let’s change our test to reflect that:

        _:test("Can set style", function()
            style.stroke(255).strokeWidth(5)
            _:expect(style.strokeWidth()).is(5)
            _:expect(style.stroke()).is({255,255,255,255})
        end)

That will fail gloriously.

1: Can set style  -- Actual: table: 0x283217740, Expected: 5
1: Can set style  -- Actual: table: 0x283245000, Expected: table: 0x283224c00

So that’s interesting. Since the functions currently return style so as to be fluent, the versions with no parameters can’t do that. We could make them return the current setting, which would flip the mode to non-fluent.

I think we’ll provide functions like getStroke().

        _:test("Can set style", function()
            style.stroke(255).strokeWidth(5)
            _:expect(style.getStrokeWidth()).is(5)
        end)

The stroke color test can’t work as written, so I removed it. We’ll see why in a moment.

This will fail with no get …

1: Can set style -- Tests:16: attempt to call a nil value (field 'getStrokeWidth')

We implement:

function style.getStrokeWidth()
    return strokeWidth()
end

That fails as expected:

1: Can set style  -- Actual: 0.0, Expected: 5
function style.strokeWidth(arg)
    style.getSettings().strokeWidth = arg
    strokeWidth(arg)
    return style
end

And the test passes. We can’t do the same test with stroke. Let’s try.

        _:test("Can set style", function()
            style.stroke(255).strokeWidth(5)
            _:expect(style.getStrokeWidth()).is(5)
            _:expect(style.getStroke()).is(3)
        end)

function style.getStroke()
    return stroke()
end

I just checked for a random 3 to see the error:

1: Can set style 255 -- Actual: 255, Expected: 3

That surprises me somewhat. I rather thought we’d see a table of RGBs. Let’s try a more complex color.

        _:test("Can set color", function()
            style.stroke(255,1,2,128)
            local r,g,b,a = style.getStroke()
            _:expect(r).is(255)
            _:expect(g).is(1)
            _:expect(b).is(2)
            _:expect(a).is(128)
        end)

This fails with a slight surprise, which is that none of the g, b, or a values are right. Of course, that’s because the stroke command isn’t dealing with multiple args. Let’s try the easy way.

function style.stroke(r,g,b,a)
    style.getSettings().stroke = r,g,b,a
    return style
end
function style.stroke(...)
    style.getSettings().stroke = {...}
    stroke(...)
    return style
end

Tests are green. Time to think, and refactor.

I am influenced here by another post on the same thread, from UberGoober if I’m not mistaken, where he was saving the values so that he could build a style object and reuse it. I am not trying to do that, and now that I’ve settled on my gets, I really don’t need my settings variable at all.

style = {}

function style.stroke(...)
    stroke(...)
    return style
end

function style.getStroke()
    return stroke()
end

function style.strokeWidth(arg)
    strokeWidth(arg)
    return style
end

function style.getStrokeWidth()
    return strokeWidth()
end

So this is interesting. Clearly I can (re)implement all the settings this way. It won’t provide me with much benefit. If it were done inside Codea Lua, it would be possible to avoid having the global stroke and strokeWidth and such, reducing namespace pollution a bit. Built on top of the existing setup, I don’t think it gives me enough value to continue. Because:

        _:test("Old style not much worse", function()
            stroke(255,1,2,128);strokeWidth(5)
            _:expect(style.getStrokeWidth()).is(5)
            local r,g,b,a = style.getStroke()
            _:expect(r).is(255)
            _:expect(g).is(1)
            _:expect(b).is(2)
            _:expect(a).is(128)
        end)

The top line shows the difference. No reference to style and semicolons instead of dots. Or, of course, one could write the statements on multiple lines.

Other than reducing namespace pollution, I’m not seeing the value.

Fun, though.

See you next time!