XST 8: Pipelines
I’m going to try to create pipelines using coroutines. I think they may make for a more expressive interface. I turn out to be partially right.
Begin with a test:
_:test("Coroutine provider", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local p = s:provider()
e,s = p:resume()
_:expect(e).is("1")
_:expect(s).is("a")
end)
This better fail asking for provider:
12: Coroutine provider -- Tests:182: attempt to call a nil value (method 'provider')
So far so good.
function XSet:provider()
return coroutine.create(function()
for e,s in self:elements() do
coroutine.yield(e,s)
end
end)
end
This mostly tells me that I don’t have the calling sequence right:
_:test("Coroutine provider", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local p = s:provider()
tf,e,s = coroutine.resume(p)
_:expect(tf).is(true)
_:expect(e).is("1")
_:expect(s).is("a")
end)
This works intermittently. Why? Because we aren’t guaranteed any particular order from our returns. Improve the test:
_:test("Coroutine provider", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local p = s:provider()
for i = 1,3 do
tf,e,s = coroutine.resume(p)
_:expect(tf).is(true)
_:expect(e=="1" or e=="2" or e=="3").is(true)
_:expect(s=="a" or s=="b" or s=="c").is(true)
end
tf,e,s = coroutine.resume(p)
_:expect(tf).is(true)
_:expect(e).is(nil)
_:expect(s).is(nil)
end)
When we call the resume a fourth time, we get a return of true, meaning no error, but nil results because we don’t have any. We could change that by returning something from the provider function. We could return “no” or “stop” or “help me”.
So far so good. I am not fond of the explicit reference to coroutine.resume
but I think we can wrap that when we get closer to what we want.
Let’s see if we can work up a selector.
_:test("Coroutine selector", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local sel = s:selector(function(e,s) return e=="1" or s=="c" end)
e,s = 1,1
while(e~=nil) do
tf,e,s = coroutine.resume(sel)
_:expect(tf).is(true)
_:expect(e=="1" or s=="c").is(true)
end
end)
Fails looking for selector, I imagine.
13: Coroutine selector -- Tests:197: attempt to call a nil value (method 'selector')
So far so good.
A bit of messing about and I have this:
_:test("Coroutine selector", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local sel = s:selector(function(e,s) return e=="1" or s=="c" end)
tf, e,s = true,1,1
while(e~=nil) do
tf,e,s = coroutine.resume(sel)
if e ~= nil then
_:expect(tf).is(true)
_:expect(e=="1" or s=="c").is(true)
end
end
end)
function XSet:selector(predicate)
return coroutine.create(function()
local prov = self:provider()
while true do
tf,e,s = coroutine.resume(prov)
if e == nil then return nil,nil end
if predicate(e,s) then coroutine.yield(e,s) end
end
end)
end
This actually works. Let’s change things up a bit.
function XSet:selector(predicate)
return coroutine.create(function()
local prov = self:provider()
while true do
tf,e,s = coroutine.resume(prov)
if e == nil then break end
if predicate(e,s) then coroutine.yield(e,s) end
end
end)
end
That continues to work, because when a coroutine exits, it just returns true, so that e and s default to nil. Now let’s modify the test similarly:
_:test("Coroutine selector", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local sel = s:selector(function(e,s) return e=="1" or s=="c" end)
while(true) do
tf,e,s = coroutine.resume(sel)
if e == nil then break end
_:expect(tf).is(true)
_:expect(e=="1" or s=="c").is(true)
end
end)
Tests still run. Let’s commit: added provider and selector functions to XSet.
I expect that we’ll just use these functions inside the XSet. Let’s see if we can use them in, oh, restrict.
function XSet:restrict(B)
-- return all elements (a) of self such that
-- there exists a record (b) in B such that
-- b:subset(a)
local result = XSet()
for a,s in self:elements() do
if B:hasSubsetElement(a,s) then
result:addAt(a,s)
end
end
return result
end
I expect this will take a few steps, but let’s see what we can do.
I’ve done something wrong. Revert.
OK, this works:
function XSet:restrict(B)
-- return all elements (a) of self such that
-- there exists a record (b) in B such that
-- b:subset(a)
local result = XSet()
--[[
for a,s in self:elements() do
if B:hasSubsetElement(a,s) then
result:addAt(a,s)
end
end
--]]
local sel = self:selector(function(e,s)
return B:hasSubsetElement(e,s)
end)
while true do
tf,e,s = coroutine.resume(sel)
if e==nil then break end
result:addAt(e,s)
end
return result
end
I commented out the old code. I don’t think this is better. I was hoping that I could get to a sort of place where I could pipeline these coroutines … or maybe treat them as iterators.
We have elements
as an iterator already. Can we do selector that way instead?
OK, here’s a new test and code that passes:
_:test("Coroutine selector", function()
local s = XSet():addAt("1","a"):addAt("2","b"):addAt("3","c")
local pred = function(e,s) return e=="1" or s=="c" end
for e,s in s:selector(pred) do
_:expect(e=="1" or s=="c").is(true)
end
end)
function XSet:selector(predicate)
return coroutine.wrap(function() self:selector_co(predicate) end)
end
function XSet:selector_co(predicate)
for e,s in self:elements() do
if predicate(e,s) then
coroutine.yield(e,s)
end
end
end
Now I think I should be able to redo restrict
to use this, and see if I like it.
function XSet:restrict(B)
-- return all elements (a) of self such that
-- there exists a record (b) in B such that
-- b:subset(a)
local result = XSet()
for e,s in self:selector(function(e,s) return B:hasSubsetElement(e,s) end) do
result:addAt(e,s)
end
return result
end
That’s nearly good. Rename selector
to select
. Inline function:
function XSet:select(predicate)
return coroutine.wrap(function()
for e,s in self:elements() do
if predicate(e,s) then
coroutine.yield(e,s)
end
end
end)
end
Now I can delete the provider and its test, it was a dead end. This is better.
I hadn’t really intended to do this, and it is getting late and dark. Let’s wrap up.
Wrapping
We’re not there yet. I think that select should return a set, not an iterator. But we already have a function elementsSuchThat that does that. Can I rewrite that to my advantage?
Not so much. How about this one:
function XSet:copyIntoSuchThat(result, predicate)
local added = false
self:elementsDo(function(e,s)
if(predicate(e,s)) then
result:addAt(e,s)
added = true
end
end)
return added
end
Yes. Saves one line:
function XSet:copyIntoSuchThat(result, predicate)
local added = false
self:elementsDo(function(e,s)
if(predicate(e,s)) then
result:addAt(e,s)
added = true
end
end)
return added
end
function XSet:copyIntoSuchThat(result, predicate)
local added = false
for e,s in self:select(predicate) do
result:addAt(e,s)
added = true
end
return added
end
Not very impressive. Maybe worth something.
I’m not deeply impressed with what I’ve got here. I think it’s a step in a nearly good direction, toward more expressive code. It’s not the bees knees, however.
Your thoughts are welcome …