4

I've been wondering whether it was possible to implement deferred execution, .NET Linq-style, in Lua, just for funsies.

In .NET, we can create a sequence of elements known as an IEnumerable. These elements can then be filtered through various means, such as map/reduce (Select(predicate), Where(predicate)), but the computation for these filters are only executed when you enumerate over the IEnumerable - it is deferred.

I've been trying to implement a similar functionality in Lua, although I'm pretty rusty with Lua and haven't touched it for a while. I'd like to avoid using libraries that already do this for me as I'd like to be able to do it in pure Lua where possible.

My idea was that perhaps it would be possible using coroutines..

Enumerable = {

  -- Create an iterator and utilize it to iterate
  -- over the Enumerable. This should be called from
  -- a "for" loop.
  each = function(self)
    local itr = Enumerable.iterator(self)
    while coroutine.status(itr) ~= 'dead' do
      return function() 
        success, yield = coroutine.resume(itr)
        if success then
          return yield
        else
          error(1, "error while enumerating")
        end
      end
    end
  end,

  -- Return an iterator that can be used to iterate
  -- over the elements in this collection.
  iterator = function(self)
    return coroutine.create(function()
      for i = 1, #self do
        coroutine.yield(self[i])
      end
    end)
  end
}


tbl = {1, 2, 3}

for element in Enumerable.each(tbl) do
  print(element)
end

table.insert(tbl, 4)

for element in Enumerable.each(tbl) do
  print(element)
end 

However, after writing this I realized this isn't really deferred execution.. this is just glorified iterators using green threads.

I'm trying to implement it to get a better understanding on how functional programming works, in a language that I am already aware of.

Thoughts?

Dan
  • 10,282
  • 2
  • 37
  • 64
  • woops, error parameters are the wrong way around. ignoring that, :) – Dan Jul 01 '14 at 19:53
  • What is your question? Lua is not a functional language. – Colonel Thirty Two Jul 01 '14 at 22:00
  • "wondering whether it was possible to implement deferred execution, .. in Lua... " – Dan Jul 01 '14 at 22:38
  • 3
    Also, @Lua not being a functional language, [section 6 of PIL](http://www.lua.org/pil/6.html) disagrees with you. It's not a pure functional language like Haskell, but it has functional elements (i.e, are first class members) and I was wondering whether it was possible to implement the notion of deferred execution in Lua, like I said in my question. I don't really see how it is "unclear what I am asking" when the premise behind my question is in the first line. – Dan Jul 01 '14 at 22:39

1 Answers1

6

The way to get deferred execution in Lua is using functions. You would need to change your API from

Where( x > 1 )

to

Where(function(x) return x > 1 end)

A full working example would be something along the lines of the following code. I left out the chaining syntax to keep it simple.

-- A stream is a function that returns a different value each time you call it
-- and returns nil after the last value is generated. Its a bit like what ipairs returns.

-- Receives a list, returns a stream that yields its values
function Each(xs)
  return coroutine.wrap(function()
    for _, x in ipairs(xs) do
      coroutine.yield(x)
    end
  end)
end

-- Receives a stream and returns a new stream, filtered by a predicate
function Where(input, pred)
  return coroutine.wrap(function()
    for x in input do
      if pred(x) then
         coroutine.yield(x)
      end
    end
  end)
end

local ys = {1,2,3,4,5}
for y in Where(Each(ys), function(x) return x <= 2 end) do
  print(y)
end

In case you are wondering how to handle the chaining, the way to do it is to have the "stream" type be an object with methods instead of a plain function.

local Stream = {}

-- The low level stream constructor receives a generator function
-- similar to the one coroutine.wrap would return. You could change the API
-- to something returning multiple values, like ipairs does. 
function Stream:new(gen)
  local stream = { _next = gen}
  setmetatable(stream, self)
  self.__index = self
  return stream
end

-- Receives a predicate and returns a filtered Stream
function Stream:Where(pred)
  return Stream:new(coroutine.wrap(function()
    for x in self._next do
      if pred(x) then
        coroutine.yield(x)
      end
    end
  end))
end


function Stream:Length()
  local n = 0
  for _ in self._next do
     n = n + 1
  end
  return n
end

function Each(list)
  return Stream:new(coroutine.wrap(function()
    for _, x in ipairs(list) do
      coroutine.yield(x)
    end
  end))
end

local ys = {10, 20, 30, 40}
print( Each(ys):Where(function(x) return x <= 20 end):Length() )

Coroutines are more about letting you write cooperating functions in a straightforward way without needing to turn one of them "inside out". For example, its perfectly possible to implement an iterator for lists without using coroutines:

-- if you try to code ipairs on your own, without coroutines
-- it might look a bit like this
function Each(xs)
  local i=1 
  return function()
    if i <= # xs then
      local x = xs[i]
      i = i + 1
      return x
    else
      return nil
    end
  end
end

Since we return a "getnext" function, we are able to only fetch one element at a time. However, we had to "blow up" the for-loop, turning it into ifs and manually updating the loop counter. We also need to keep track of all the iteration state explicitly. In this case its just the loop counter but in coroutines with recursion you would need to keep a stack and if the coroutine has more than one yield in its body then you need some state flag to do the job of the program counter.

hugomg
  • 68,213
  • 24
  • 160
  • 246
  • So, wait, I can do `for x in func` if the `func` returns a coroutine? (referring to `return coroutine.wrap`, I've never used this function). And if this is the case, how could I handle the continuation of functions? That was the main focus in this case - to be able to do something like: `source:where(function(x) return true end):select(function(x) return x * 2):accumulate(function(x, y) return x + y end)` – Dan Jul 02 '14 at 13:55
  • Check out the docs for the [for statement](http://www.lua.org/manual/5.2/manual.html#3.3.5). `func` is a function and there isn't anything magical to do with coroutines - I am using the same interface that pairs and ipairs use. As for [coroutine.wrap](http://www.lua.org/manual/5.2/manual.html#pdf-coroutine.wrap) its a helper that returns a function wrapping the `coroutine.resume` instead of a direct reference to the coroutine. – hugomg Jul 02 '14 at 16:50
  • As for getting a chaining syntax,. its just a matter of using an OO interface, where `Where` and `Select` are methods of the stream objects, instead of using plain old functions. There is more than one way you can do this (search a bit for Lua object orientation) but I added my answer to include an example. – hugomg Jul 02 '14 at 17:15
  • Finally, I would like to emphasize that the important thing for deferring code is functions and that there is no fundamental need to be using coroutines or `for x in blah` tricks here. If you wanted you could implement everything using plain objects and functions, as illustrated by that inside out `Each` function I showed at the end. – hugomg Jul 02 '14 at 17:18
  • > and that there is no fundamental need to be using coroutines or for x in blah tricks here Yeah I can understand this, I was trying to emulate the IEnumerable interface which only actually executes when it is enumerated, hence the interest in deferring until enumeration :P. I don't think there was any need for coroutines either, it just felt like the most obvious fit – Dan Jul 02 '14 at 17:19