PiLisp: pl> Macro

PiLisp's REPL wraps everything you submit to it with an invocation of its pl> macro.

The pl> macro expects expressions to be separated by the pipe | symbol. The forms between pipe separators are wrapped in an implicit pair of parentheses, meaning that they are invoked, and the return value of each pipe-delimited expression is threaded as the last argument to the next expression:

pl> bindings | keys | sort | take 10
(
  *
  *1
  *2
  *3
  *e
  *pilisp-version*
  +
  -
  ->
  ->>
)

The initial form is invoked only if it is a function or a symbol that resolves to one, so that other types of values can be entered directly:

pl> [0 1 2 3 4 5 6 7 8 9] | map (partial * 3) | filter even?
(
  0
  6
  12
  18
  24
)

Finally, if you want to have the previous expression's value placed in a non-final position, you can use the $ symbol:

pl> "hello" | dart/String.toUpperCase | str $ " world!"
"HELLO world!"

A desire—at the REPL—to write programs in this style by default was part of my initial inspiration for building PiLisp's predecessor sc which had this syntax hard-coded into the language implementation. One of the most satisfying moments of PiLisp's development was implementing macro support to the point where I could write pl> as a macro in PiLisp itself.

This syntactic approach of pl> presents problems when a program spans multiple lines, but at an interactive REPL, the ability to append forms and further refine a program by pressing the up key followed by | leverages years of CLI/shell muscle memory and results in a low-friction experience for small experiments. It also naturally leads to thoughts about mirroring the system shell in other ways, e.g., by supporting a notion of "current directory" as Ruby's pry does. I did this for sc, allowing one to "step into" a Shortcut story, epic, iteration, milestone, etc, and PiLisp currently supports a rudimentary facility for using cd to "step into" data at the REPL.

In my own Clojure development, I have moved away from using interactive REPLs directly when writing larger programs, preferring to interact with the REPL via code in files. For code bases with multiple files, dependencies, and a lot of different classpath and conceptual contexts to navigate, I find this workflow superior to typing at the Clojure REPL directly.1

But PiLisp is designed for smaller programs. A fast-to-start Lisp environment that provides a CLI REPL by default, with access to the Dart ecosystem when your experiments need to expand, and that can be compiled to a standalone executable by the built-in toolchain2 has filled several gaps for me that Clojure on the JVM alone has not been able to fill.


  1. If the REPL were a more powerful interactive partner, this might not be the case.

  2. Babashka for Clojure specifically and native-image on GraalVM for the JVM fill these gaps. Part of the challenge is that these are not part of the standard Java or Clojure toolchains and in the past configuring builds for native executables using these tools has involved build and configuration twiddling that I've found frustrating.


Tags: clojure programming-language pilisp cli dart macro

Copyright © 2024 Daniel Gregoire