Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sequential procedures in Lisp

When I try to program in a functional style with immutable objects, sequential operations end up being written inside-out, like this:

(thing-operation3
  (thing-operation2
    (thing-operation1 thing extra-arg1)
    extra-arg2)
  extra-arg3)

I'm starting to see this pattern repeating all over my code, and I find it very hard to read. This could marginally be improved using higher-order procedures like curry and compose:

((compose1
  (curryr thing-operation3 extra-arg3)
  (curryr thing-operation2 extra-arg2)
  (curryr thing-operation1 extra-arg1))
 thing)

Better perhaps, but it is still written upside-down, and it takes some extra cognitive load to figure out what is going on. And I'm not sure whether this is ideomatic Lisp-code.

Object-oriented style is so much easier to read:

thing.operation1(extra-arg1).operation2(extra-arg2)
    .operation3(extra-arg3)

It reads in a natural order, and it could also be implemented with immutable objects.

What is the ideomatic way of writing such sequential operations in Lisp so that they are easy to read?

like image 803
alols Avatar asked Dec 10 '22 22:12

alols


1 Answers

An usual way in Common Lisp would be to use LET*

(let* ((thing1 (thing-operation0 thing0 extra-arg0))
       (thing2 (thing-operation1 thing1 extra-arg1))
       (thing3 (thing-operation2 thing2 extra-arg2)))
  (thing-operation3 thing3 extra-arg3))

That way one can name the return values, which improves readability and one could write declarations for those.

One could also write a macro which might be used like in the following:

(pipe
 (thing-operation1 thing extra-arg1)
 (thing-operation2 _2    extra-arg2)
 (thing-operation3 _3    extra-arg3)
 (thing-operation4 _4    extra-arg4))

Some language provide similar macros and Lisp libraries may provide variations of it. Let's write a simple version of it:

(defmacro pipe (expression &rest expressions)
  (if (null expressions)
      expression
    (destructuring-bind ((fn arg &rest args) &rest more-expressions)
        expressions
      (declare (ignorable arg))
      `(pipe
        (,fn ,expression ,@args)
        ,@more-expressions))))

For above pipe expression the following code is produced:

(THING-OPERATION4
 (THING-OPERATION3
  (THING-OPERATION2
   (THING-OPERATION1 THING EXTRA-ARG1)
   EXTRA-ARG2)
  EXTRA-ARG3)
 EXTRA-ARG4)

A variant:

(defmacro pipe (expression &rest expressions)
  (if (null expressions)
      expression
    (destructuring-bind ((fn arg &rest args) &rest more-expressions)
        expressions
      `(pipe
        (let ((,arg ,expression))
          (,fn ,arg ,@args))
        ,@more-expressions))))

This would let you write:

(pipe (+ 1000 pi)
      (+ arg1 arg1)         ; use the previous result multiple times
      (+ arg2 (sqrt arg2))) ; use the previous result multiple times
like image 142
Rainer Joswig Avatar answered Dec 19 '22 19:12

Rainer Joswig