I don’t love the interface for adding functions into an XSet. And I want to add them ‘one level in’. Will hilarity ensue? Probably not, but something will happen.

As things stand, given a set S, we can provide a table of functions to create a new set that has those functions added as if they were set elements. The syntax looks like this:

            local S = XSet:fromTable{a=5,b=50}
            local functions = {}
            functions.sum = function(r)
                return r:at("a") + r:at("b")
            end
            functions.prod = function(r)
                return r:at("a") * r:at("b")
            end
            local F = XSet:withFunction(S,functions)

Here, withFunctions is a class method. I’ve already forgotten to pass in the base set at least once. However, we could make it work like this:

            local F = S:withFunction(functions)

Fact is, I thought it would just work, but in fact it says this:

7: Multiple Function Elements -- XSet:174: XData:178: attempt to call a nil value (method 'elements')

I am surprise. Let’s look at the method:

function XSet:withFunction(S,F)
    return XSet:on(XFunction(S,F))
end

Ah. We need to remove the S from the parameter list. We can’t quite have it both ways. Let’s have it the new way.

function XSet:withFunction(F)
    return XSet:on(XFunction(self,F))
end

And let’s rename the method to withFunctions.

function XSet:withFunctions(F)
    return XSet:on(XFunction(self,F))
end

That should also conveniently point to the users of the old style. There’s just this one test now. I vaguely recall that about this time yesterday I consolidated down to one test. Should run now.

And it does.

The creator of WorkingCopy provided excellent support on New Year’s day, providing me with a fix for the problem I encountered with the app, so now I have the ability to commit: Change class method withFunction to instance method withFunctions.

Unpaid Endorsements

WorkingCopy is an excellent product, and if you have any need for Git on iOS, I highly recommend it.

Codea, it goes without saying, is marvelous as well.

Where were we?

I am curious whether we can apply functions on top of functions. I would expect that to work. Let’s find out.

        _:test("Multiple Layers of Functions", function()
            local S = XSet:fromTable{a=5,b=50}
            local f1 = {}
            f1.sum = function(r)
                return r:at("a") + r:at("b")
            end
            local f2 = {}
            f2.prod = function(r)
                return r:at("a") * r:at("b")
            end
            local F = S:withFunctions(f1):withFunctions(f2)
            _:expect(F:card(),"card").is(4)
            _:expect(F:hasAt("55","sum")).is(true)
            _:expect(F:hasAt("250","prod")).is(true)
            local output = {}
            for value,name in F:elements() do
                table.insert(output, name.."="..value)
            end
            _:expect(output).has("prod=250")
            _:expect(output).has("sum=55")
            _:expect(output).has("a=5")
            _:expect(output).has("b=50")
        end)

This test runs perfectly, just as I expected. Commit: test to verify multiple layers of withFunction.

So that’s nice. However, what we want, what we really really want, is to apply our functions “one level down”. That is, given a set of records, we want the records to act as if the functions are fields in them, not fields of the outer set.

XFunction does the inside part if it is invited to. We need now, to provide a class that issues the invitation.

It should be easy. We’ll need a new function, whose name is harder to think of than the code, perhaps withInnerFunctions will do for now. That function will return a new class instance, of the kind that applies the functions to each XSet element that it returns.

We need a test.

But wait. Remember yesterday where I got in a bit of extra trouble with a test at a higher level than necessary. I just accepted whatever failure it gave as telling me what I thought would happen, but in fact the error was elsewhere. Let’s actually TDD the new thing.

        _:test("XInnerFunction", function()
            local A = XSet:fromTable({ {a=5}, {a=50} })
            local f = {}
            f.double = function(r) return r:at("a")*2 end
            local doubler = XInnerFunction(A,f)
            for e,s in doubler:elements() do
                if e:at("a") == "5" then
                    _:expect(e:at("doubler")).is("10")
                elseif e:at("a") == "50" then
                    _:expect(e:at("doubler")).is("100")
                end
            end
        end)

That looks right to me. Let’s make it go. Should fail looking for XInnerFunction.

9: XInnerFunction -- XData:126: attempt to call a nil value (global 'XInnerFunction')

Let’s code a bit:

XInnerFunction = class(XData)

function XInnerFunction:init(Set, FunctionTable)
    self.set = Set
    self.fcn = FunctionTable
end

Should fail … let’s see … looking for elements.

9: XInnerFunction -- XData:159: Must implement elements!

OK can do:

function XInnerFunction:elements()
    -- return iterator
    return coroutine.wrap(function()
        for record, scope in self.set:elements() do
            coroutine.yield(record:withFunctions(self.fcn), scope)
        end
    end)
end

I really thought that would work, but no:

9: XInnerFunction  -- Actual: nil, Expected: 10
9: XInnerFunction  -- Actual: nil, Expected: 100

First check my spelling. Ha. The function’s name is “double” not “doubler”. Fix the test:

        _:test("XInnerFunction", function()
            local A = XSet:fromTable({ {a=5}, {a=50} })
            local f = {}
            f.double = function(r) return r:at("a")*2 end
            local doubler = XInnerFunction(A,f)
            for e,s in doubler:elements() do
                if e:at("a") == "5" then
                    _:expect(e:at("double")).is("10")
                elseif e:at("a") == "50" then
                    _:expect(e:at("double")).is("100")
                end
            end
        end)

Now this better run. And it does. Take that, Fate!

Commit: Implement XInnerFunction, XData subclass that applies functions to set elements one rank below.

I typed “to set elements” there because as I typed the commit message I realized that bad things will happen if we add a raw element to the input. Let’s change the test to do that:

        _:test("XInnerFunction", function()
            local A = XSet:fromTable({ a="error",{a=5}, {a=50} })
            local f = {}
            f.double = function(r) return r:at("a")*2 end
            local doubler = XInnerFunction(A,f)
            for e,s in doubler:elements() do
                if e:at("a") == "5" then
                    _:expect(e:at("double")).is("10")
                elseif e:at("a") == "50" then
                    _:expect(e:at("double")).is("100")
                end
            end
        end)

This will try to apply withFunctions to the string “error”, which is unlikely to work:

9: XInnerFunction -- XData:127: XData:203: attempt to call a nil value (method 'withFunctions')

We need to ensure that we only wrap the XSets, not other random stuff that may appear.

function XInnerFunction:elements()
    -- return iterator
    return coroutine.wrap(function()
        for record, scope in self.set:elements() do
            if record.withFunctions then record = record:withFunctions(self.fcn) end
            coroutine.yield(record, scope)
        end
    end)
end

This solves one problem but breaks the test in a new and fascinating way:

9: XInnerFunction -- XData:128: attempt to call a nil value (method 'at')

Our loop now includes the string error with scope “a” so we can’t send at to that.

        _:test("XInnerFunction", function()
            local A = XSet:fromTable({ a="error",{a=5}, {a=50} })
            local f = {}
            f.double = function(r) return r:at("a")*2 end
            local doubler = XInnerFunction(A,f)
            for e,s in doubler:elements() do
                if type(e)=="string" then
                    -- do nothing
                elseif e:at("a") == "5" then
                    _:expect(e:at("double")).is("10")
                elseif e:at("a") == "50" then
                    _:expect(e:at("double")).is("100")
                end
            end
        end)

Now then. I didn’t really check to see whether record was an XSet. I checked to see if it understands withFunctions, and didn’t even check whether what came back was a method. What we need is a solid way of checking whether something is an XSet. We’ve put various kludges around the system. Unfortunately, we can’t check easily, because a class or instances is a table, and not all tables are classes and not all instances are XSets.

It’ll be rather messy. Let’s write a test for it. Don’t let me forget to bump withInnerFunctions up to XSet before we’re done tho.

        _:test("IsXSet", function()
            _:expect(XSet:isXSet(5)).is(false)
            _:expect(XSet:isXSet("a")).is(false)
            _:expect(XSet:isXSet({})).is(false)
            _:expect(XSet:isXSet(XSet())).is(true)
        end)

This will fail for want of the method.

2: IsXSet -- Tests:20: attempt to call a nil value (method 'isXSet')

Write it. I’ll just go for it.

function XSet:isXSet(candidate)
    if type(candidate)~="table" then return false end
    local m = getmetatable(candidate)
    if not m then return false end
    if not m.is_a and not candidate.is_a then return false end
    return candidate:is_a(XSet)
end

The test runs. Commit: XSet:isXSet() implemented.

Let’s clean that method up iteratively. No, let’s not. It’s clear enough as it stands and while the fancy and/or returns are often more clear, I don’t think that’ll happen here.

We still need our XSet-level withInnerFunctions.

Write a test:

        _:test("XSet inner functions", function()
            local A = XSet:fromTable({ a="error",{a=5}, {a=50} })
            local f = {}
            f.double = function(r) return r:at("a")*2 end
            local D = A:withInnerFunctions(f)
            for e,s in D:elements() do
                if type(e)=="string" then
                    -- do nothing
                elseif e:at("a") == "5" then
                    _:expect(e:at("double")).is("10")
                elseif e:at("a") == "50" then
                    _:expect(e:at("double")).is("100")
                end
            end
        end)

Same as the other except it creates a set. Expect the method to be missing.

10: XSet inner functions -- XData:142: attempt to call a nil value (method 'withInnerFunctions')

Implement. I think this should do it:

function XSet:withInnerFunctions(F)
    return XSet:on(XInnerFunction(self,F))
end

And indeed it does. Commit: XSet understands withInnerFunctions to apply a table of functions to all the XSet elements (records) contained in the set.

It’s Sunday, it’s snowy, it’s nearly time for first breakfast. Let’s sum up.

Summary

Strangely, what happened this morning was almost according to plan. We did divert briefly to take on an opportunity to implement isXSet. I think there may be other code lying about that could make use of that, but I’m not going to look for it just now. Other than that, we improved the calling sequence for withFunctions and implemented withInnerFunctions. The latter is likely the only one people will use, but you never know.

So far, the year is going well. WorkingCopy is back in hand, thanks to Anders, and the set operations continue to grow in power.

I’m not sure what we might do next. It may be about time to work on a higher-level “language” to define operations, and to work out how to do optimizations within that language. I think that will be challenging and perhaps fun.

We’ll see, next time. Do stop by.


XSet2.zip