Define your own macros
Beyond the built-ins (map, filter, fork, when, …) pipescript
lets you define your own macros. Define one in a cell and it is registered for
use in the cells that follow. There are two ways to write one: with the macro
DSL, or with a custom node transformer.
With the macro DSL
The macro[...] and method[...] constructors build a macro out of a
pipescript template, using $$ to mark where the macro’s argument should be
spliced in. For example, a switch macro that runs its branches as a fork
and collapses to the single non-null result:
>>> switch = macro[fork[$$] .> collapse]
>>> 1 |> switch[when[$ == 0] .> $ + 1, when[$ == 1] .> $ - 1]
0
>>> 0 |> switch[when[$ == 0] .> $ + 1, when[$ == 1] .> $ - 1]
1
$$ may be named ($$v) when the same argument is spliced in more than one
place, and a macro can take several arguments by listing their names after the
template:
# swap a pair of values
>>> flip = macro[($$b, $$a), a, b]
>>> flip[0, 1]
(1, 0)
Macros can be defined in terms of other macros (expansion is recursive), and
method[...] defines a macro you invoke as a method on its first argument –
exactly how the built-in foreach is defined
(method[$$ |> map[do[$$]] |> list]; see Use method macros).
With a custom node transformer
When a template is not expressive enough, hand macro[...] a Python callable
instead. pipescript looks the name up in your namespace and, at expansion time,
calls it with the AST node written inside the brackets; whatever expression
you return is spliced in (it may itself be pipescript, e.g. an f[...] quick
lambda). Because the macro sees its argument’s syntax, it can do things a
template cannot – such as reading the argument’s source text.
Here is a dbg-style macro (à la Rust’s dbg!) that prints an expression
next to its value and then evaluates to it:
import ast
def show(node):
# `node` is the AST of whatever appears inside `dbg[...]`. Read its source
# text and emit `(print("<src> =", <expr>), <expr>)[1]` -- which prints the
# labeled value, then evaluates to it.
label = ast.Constant(ast.unparse(node) + " =")
printed = ast.Call(ast.Name("print", ast.Load()), [label, node], [])
pair = ast.Tuple([printed, node], ast.Load())
result = ast.Subscript(pair, ast.Constant(1), ast.Load())
return ast.fix_missing_locations(ast.copy_location(result, node))
dbg = macro[show]
>>> x = 5
>>> dbg[x * 2 + 1]
x * 2 + 1 = 11
11
>>> dbg[x * 2 + 1] |> $ + 100 # works mid-pipeline too
x * 2 + 1 = 11
111
The callable can equally be a class implementing __call__(self, node) when
it needs to carry state while inspecting the node.
Namespace-block macros
pipescript also supports namespace-block macros, which run a brace block and
harvest its top-level assignments into a dict before handing them to a builder
function. Register one with register_namespace_macro from
pipescript.tracers.macro_tracer.