Yet Another Threading Macro

Pipelines With Pleasant Exits

In which a threading macro is demonstrated that provides syntax for early exit, per-step variable bindings, and arbitrarily formed steps.

Here's What I Made

Don't as me why. I think I was looking at discord and somebody asked for a way to, essentially, get Rust/Haskell's Result-like composition in Common Lisp. This isn't quite how to do that, but it gets you some of the way there.

What I made instead is a threading macro, called >>, that gives you a handy exit early syntax. Here are some examples:

     ;; define a function  
     (defun div-random (n r)  
       (>> ()  
           (random r)         ; start with a random number  
           :? (not (zerop _)) ; check that it is not zero  
           (/ n _)))          ; and divide by it  
     ;; run it a few times  
     > (div-random 10 3)  
     > (div-random 10 3)  
     > (div-random 10 3)  

Obviously this isn't a very interesting example, but you can already see two neat tricks.

First, the use of the :? syntax. This treats the next form as a predicate on the output of the previous form. If that predicate returns NIL, the whole thing exits, returning the value of a keyword argument :fail that defaults to NIL.

Next, the use of _ to create a variable that receives the value of the previous expression in the pipeline.

It should be noted that, when a :? form returns true, then it simply passes the value it tested on to the next form.

Variable Tweaks

In the above, only _ was used as a "substitution variable" to receive the output of the previous step. But there is more to variables than that.

Naming Substitution Variables

First, you can name variables. So _x, and _my-var are also variables. This feature is supported to improve legibility of pipelines when appropriate.

Custom Variable Prefixes

In fact, the general format for a variable is PREFIX[NAME], and you can set the prefix to whatever you like. For example:

     ;; here we set variables to be prefixed by ?  
     (>> (:prefix ?)  
         (* ?x 3))  

In the above, we set the variable prefix to ?. This lets us choose a prefix in the off chance that _ will cause some kind of conflict, possibly treating an unwanted symbol as a substitution variable.

In the above example, we also snuck the 1+ function into our pipeline. In general can use symbols for which fboundp is T in our pipelines, but we CANNOT use symbols bound to a function in the variable name space.

Repeated Variables

Within any single pipeline form, you can repeat the variable it receives as many times as you like. E.g.

     ;; a contrived example  
     (>> ()  
         (random 10)  
         (list _r :+ _r := (+ _r _r)))  
     (4 :+ 4 := 8)  

Under the hood, the macro detects repeated variables. When it finds them, it wraps the expression in a let form and binds the value of the previous expression to a temporary hidden variable. That temporary hidden variable is then substituted into the current form wherever the substitution variable appears.

Some threading macros bind every expression in the pipeline to such a hidden variable, all inside a big let* form. Other threading macros simply substitute the entire previous form into a specific spot in the following form. This macro combines the two approaches: it only uses let bindings when it needs to, and if it never does, it substitutes the entire expression into the next form at the right spot.

Custom Failure

When a :? form returns NIL, by default so does the entire expression. However, you can customize the "failure expression" using the :fail keyword.

Consider the following:

     ;; custom failure return  
     (>> (:fail (list :ohno (random 1.0)))  
         (random 2)  
         :? evenp) 

The above expression will return 0 half the time. The other half of the time it will return a list that looks like (:ohno 0.84352255).

Controlled Early Returns

If for some bizarre reason you want to return early without using a :? check, you can do so using the :block keyword argument and an good ole return-from form.

     ;; a little silly  
     (>> (:block wut)  
         (concatenate 'string _ " is a number")  
         (if (< (length _s) 100)  
             (return-from wut)  

In the above, NIL is returned b/c "10 is a number" is shorter than 100 characters in length.

But Really Why?

Some people have bizarrely strong opinions about threading/piping macros. I think they're neat. They're especially great for fetching nested data.

Suppose you've got a CLOS object with slots that are other CLOS objects with plist slot values whose values are themselves other CLOS objects.

Threading macros are handy for what would otherwise be illegible and onerous slot access.

For example:

   ;; nice?  
     (>> ()  
         :? bar-ok-p  
         (getf _zoo :animal)  
         :? (animal-heaver-than-p _ 100)  

Might be easier to read than

     ;; it is a matter of preference  
     (let ((bar  
       (when (bar-ok-p bar)  
         (let ((animal  
                 (getf (zoo-reader bar) :animal)))  
           (when (animal-heaver-than-p animal 100)  
             (blar-reader animal)))))  

That Is All

So yeah. I'm not totally sure why I made this. I like the innovation of using the :? syntax, and it makes me think the macro could be adapted to include other odd syntaxes.

You can find the source code here.

As ever, I hope you have enjoyed this thoroughly self-indulgent episode of Parenthetical Curios.