Is there a way to write function to compose functions in Elisp?
Asked Answered
T

3

8

I wanted to create a function, that returns a composition of several other functions, such that

(funcall (compose 'f 'g) x) == (f (g x))

I feel that I miserably failed on this. My best attempt so far:

(defun compose (funcs)
  "composes several funcitons into one"
  (lambda (arg)
    (if funcs
        (funcall (car funcs) (funcall (compose (cdr funcs)) arg))
        arg)))

But for some reason, the following still returns 0:

(funcall (compose '(
           (lambda (a) (* a 3))
           (lambda (a) (+ a 2))
)) 0)

Is there a way to fix that?

Tabanid answered 1/11, 2012 at 9:58 Comment(0)
H
7

Your code requires lexically scoped lambdas, which Emacs Lisp doesn't support by default. If you use Emacs 24, set lexical-binding to t and your example will work as written.

If you're using an older Emacs, you can create a lexically scoped lambda using an explicit lexical-let form:

(require 'cl)

(defun compose (funcs)
  "composes several funcitons into one"
  (lexical-let ((funcs funcs))
    (lambda (arg)
      (if funcs
          (funcall (car funcs) (funcall (compose (cdr funcs)) arg))
        arg))))
Hassanhassell answered 1/11, 2012 at 11:7 Comment(2)
So in my original code, lambda didn't close over funcs, so it's value was nil on time of execution?Tabanid
Undefined values are an error in elisp. Indeed, when I ran your code unmodified, I got a (void-variable funcs) error. Presumably funcs was accidentally left defined as a global variable in your environment, giving you the impression that the code doesn't work due to an unrelated bug.Hassanhassell
L
0

In case you want a library, see dash functional, it has -compose taking (&rest fns).

Littell answered 23/9, 2020 at 19:40 Comment(1)
Nowadays dash-functional is an empty package, all the functionality from it has been moved into dash.Cubbyhole
C
0

For those not in love with the other answers, here's a compose macro which

  1. accepts any number of arguments for the innermost function,
  2. has no dependencies (neither a package nor turning on lexical binding), and
  3. expands more like you'd hand-write it:
(defmacro compose (&rest functions)
    (setq functions (nreverse functions))
    (let ((form `(apply ,(car functions) arguments)))
        (while (setq functions (cdr functions))
            (setq form `(funcall ,(car functions) ,form)))
        `(lambda (&rest arguments) ,form)))

This also illustrates an alternative approach: instead of creating a recursive lambda which has to remember the function list and go through it each time it's called, the entire function list is expanded into the lambda form once.

So for example, (compose 'f 'g 'h) expands to:

(lambda (&rest arguments)
    (funcall 'f (funcall 'g (apply 'h arguments))))
Cubbyhole answered 30/1 at 3:9 Comment(3)
Nice! One nitpick - I think it still needs lexical binding, because you can rebind the symbol after composing, resulting in behavior change for composed function: ``` (defun f (a) (* a 3)) (defun g (a) (+ a 2)) (setq c (compose 'f 'g)) (defun f (a) (+ a 2)) (funcall c 12) ;; returns 16 instead of 42 ```Tabanid
@Tabanid yes - same as with a lambda or a defun. To me this consistency is a benefit.Cubbyhole
@Tabanid We can also wrap use of this compose in some scope (whether lexical or dynamic) and bind symbols' function values into variables in that scope, or use a non-interned locally scoped symbol (which basically nothing could override unless it went out of its way to somehow extract it from the composed lambda) to bind the function. So in your example, we could do (let ((s (make-symbol "s"))) (setq c (compose s 'g)) (setf (symbol-function s) (symbol-function 'f))) between the two (defun f ...). So I think this compose is a more flexible foundation.Cubbyhole

© 2022 - 2024 — McMap. All rights reserved.