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 …