Composing two functions in lua

I just started learning lua, so what I'm asking may not be possible.

Now I have a method that takes a function:

function adjust_focused_window(fn)
  local win = window.focusedwindow()
  local winframe = win:frame()
  local screenrect = win:screen():frame()
  local f, s = fn(winframe, screenrect)
  win:setframe(f)
end

      

I have several functions that accept these frames and rectangles (only showing one):

function full_height(winframe, screenrect)
   print ("called full_height for " .. tostring(winframe))
  local f = {
     x = winframe.x,
     y = screenrect.y,
     w = winframe.w,
     h = screenrect.h,
  }
  return f, screenrect
end

      

Then I can do the following:

hotkey.bind(scmdalt, '-', function() adjust_focused_window(full_width) end)

      

Now, how could I create multiple functions for adjust_focused_window

without changing its definition. Something like:

hotkey.bind(scmdalt, '=', function() adjust_focused_window(compose(full_width, full_height)) end)

      

where compose2

returns a function that takes the same parameters as full_width

and full_height

, and internally does something like:

full_height(full_width(...))

      

+3


source to share


1 answer


As mentioned in the comments, to combine two functions together, you can simply:

function compose(f1, f2)
  return function(...) return f1(f2(...)) end
end

      

But what if you want to combine more than two functions? You may ask: is it possible to "compose" an arbitrary number of functions together?

The answer is a definite yes - below I show 3 different approaches for doing this, as well as a summary of their implications.

Iterative table approach

The idea here is to call each function in the list one by one in turn. With this, you save the returned results of the previous call to a table, and you unpack that table and pass it to the next call.

function compose1(...)
    local fnchain = check_functions {...}
    return function(...)
        local args = {...}
        for _, fn in ipairs(fnchain) do
            args = {fn(unpack(args))}
        end
        return unpack(args)
    end
end

      

The helper check_functions

above just checks that the passed material is actually functional - an error occurs if not. The implementation has been omitted for brevity.

+ : Reasonably direct approach. You probably came up with it on the first try.

- : not very efficient use of resources. Lots of garbage tables to store results between calls. You also need to deal with packing and unpacking the results.

Y-Combinator Template



The key insight here is that while the functions we call are not recursive, it can be made recursive by supporting it with a recursive function.

function compose2(...)
  local fnchain = check_functions {...}
  local function recurse(i, ...)
    if i == #fnchain then return fnchain[i](...) end
    return recurse(i + 1, fnchain[i](...))
  end
  return function(...) return recurse(1, ...) end
end

      

+ : does not create additional temporary tables as above. Thoroughly written as tail-recursive - this means that extra stack space is not required for calls to long function chains. There is a certain elegance there.

Generating meta-script

Using this last approach, you are using a lua function that actually generates the exact lua code that executes the whole chain of function calls.

function compose3(...)
    local luacode = 
    [[
        return function(%s)
            return function(...)
                return %s
            end
        end
    ]]
    local paramtable = {}
    local fcount = select('#', ...)
    for i = 1, fcount do
        table.insert(paramtable, "P" .. i)
    end
    local paramcode = table.concat(paramtable, ",")
    local callcode = table.concat(paramtable, "(") ..
                     "(...)" .. string.rep(')', fcount - 1)
    luacode = luacode:format(paramcode, callcode)
    return loadstring(luacode)()(...)
end

      

loadstring(luacode)()(...)

may need some explanation. Here I decided to code the functional chaining as parameter names ( P1, P2, P3

etc.) in the generated script. The extra parenthesis ()

is to "unwrap" nested functions, so the inner majority function is what was returned. The parameters P1, P2, P3 ... Pn

become captured by upvalues ​​for each of the functions in the chain, for example.

function(...)
  return P1(P2(P3(...)))
end

      

Note that you could also have done it with setfenv

, but I took this route to avoid changing the interrupt between lua 5.1 and 5.2 on how function environments are set.

+ : avoids additional intermediate tables such as approach # 2. Doesn't abuse the stack.

- : Requires additional bytecode compilation step.

+3


source







All Articles