Search code examples
luafunctional-programmingcode-structure

How to thread, pipe or compose functions in lua?


What to do in lua if I want to pass a value to foo(), pass it's return to bar(), pass it's return to baz() etc?

Exhibit A (pseudocode):

backwards(code(reading(like(not(do(I))))))

Exhibit B (pseudocode):

local all = foo(...)
local this = bar(all)
local variables = baz(this)
local obscure_a_simple_idea = quux(variables)

In F# this code (source)

let numbers = [0..100]
let evenNumbers = List.filter isEven numbers
let doubledNumbers = List.map double evenNumbers
List.iter printNumber doubledNumbers

can be rewritten like this:

[0..100]
|> List.filter isEven
|> List.map double
|> List.iter printNumber

In clojure this code (source)

(defn calculate []
   (reduce + (map #(* % %) (filter odd? (range 10)))))

Can be written like this

(defn calculate* []
   (->> (range 10)
        (filter odd? ,,,)
        (map #(* % %) ,,,)
        (reduce + ,,,)))

I am aware that fennel has threading macros. What I would like to know: what can I do in lua? Did I miss a language feature? Is there some well-known ideom I am not aware of? Am I stuck choosing between exhibit A, exhibit B, or migrating to fennel?


Solution

  • Good news: Lua has first-class functions so pretty much all of this is possible in plain Lua! (It may be more verbose and/or inefficient, though.)

    Let's start with function composition:

    function compose(f, g)
        return function(...)
            return g(f(...))
        end
    end
    

    I've flipped f and g here, so f(g(x)) would be equivalent to compose(g, f)(x).

    Let's write a left fold on a vararg:

    function foldl(f, acc, ...)
        if select("#", ...) == 0 then
            return acc
        end
        local l = ...
        return foldl(f, f(acc, l), select(2, ...))
    end
    

    Let's also define the identity function:

    function id(...) return ... end
    

    Now let's take care of Exhibit A:

    function chain(...) return foldl(compose, id, ...) end
    chain(dont, like, reading, code, backwards)(I)
    

    Since chaining is just composition with another order, it returns a function, so the argument is still on the right. If you're not happy with this, you could write yourself a function which takes the argument and does the call, similar to the threading F# / Clojure have to offer:

    function chaincall(arg, ...) return chain(...)(arg) end
    

    Now your Exhibit A becomes chaincall(I, dont, like, reading, code, backwards).

    Exhibit B is pretty much the same: You can do chain(foo, bar, baz, quux)(...). chaincall won't work with a vararg as first parameter, though: The vararg would be truncated to its first value.

    Voilà! Essentially we've implemented Clojure's ->> now: chaincall(x, ...) is equivalent to (->> x ...).

    F#'s |> is somewhat harder to do, because it requires us to somehow deal with a list of arguments to follow the "threaded" argument per function. One way to do it would be using a "right curry" which appends arguments to a function call:

    function curry_right(f, ...)
        local args, n_curried = {...}, select("#", ...)
        return function(...)
            local n = select("#", ...)
            local concat = {...}
            for i = 1, n_curried do
                concat[n + i] = args[i]
            end
            return f(table.unpack(concat, 1, n + n_curried))
        end
    end
    

    Then you could just do:

    chaincall(range(0, 100),
        curry_right(List.filter, isEven, numbers),
        curry_right(List.map, double, evenNumbers),
        curry_right(List.iter, printNumber, doubledNumbers))
    

    similar to F# (assuming suitable definitions of range etc.)


    As is often the case in Lua, it gives you the power to implement this yourself, pushing tradeoffs and design decisions on you.

    Even with the simple implementations I have provided, there are discussions to be had:

    • How to namespace these functions. For simplicity, I'm just "polluting" the global namespace here.
    • How to implement these functions. You could consider directly implementing a compose_multiple using a table of functions, for example. You could consider using the identity function and making a fold which requires a vararg with at least one element.
    • How / whether to optimize these functions. I brushed performance concerns aside here.

    A related library worth mentioning is luafun, which provides functions like filter and map. That library also showcases another frequently chosen way of (emulating) this "chaining": By means of "methods" on "objects" (tables with metatables) which in turn return the objects. For example you could have an "iterator" object. Calling iterator:filter(f) would return a new iterator, on which you could then call map: iterator:filter(f):map(g) etc.


    Using Lua's metatables, you could even get some syntactic sugar. Let's repurpose the right shift operator (f >> g) for "flipped" function composition:

    debug.setmetatable(function()end, {__shr = function(g, f) return function(...) return f(g(...)) end end})
    function square(x) return x^2 end
    (square >> print)(4) -- 16.0
    

    (In older Lua versions, you'd have to use a different operator. Also note that this probably isn't the best idea in "serious" code due to performance and readability concerns; such "abuse" of the debug library is usually frowned upon in "serious" code, and many "safe" environments may remove or restrict the debug library altogether. Static analysis tools might also get confused.)