XST 35: Moar Functions
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.