🏠

the pipe |

one character. the whole library hangs off it.

In Python, | is bitwise OR. You knew this. 5 | 3 is 7. What Zef does:

Zef hijacks __or__ on its own types so that data | op means "apply op to data." If either side is a regular Python value, Python's bitwise OR still runs. No monkey-patching of built-ins β€” Zef only intercepts when at least one side is a Zef value.

every ZefOp can be called two ways

This is a small but important rule. Every ZefOp accepts its data-flow argument first, and the other args curried:

eager β€” plain function
trim('abcd', 'a')
# => 'bcd'
lazy β€” pipeline
'abcd' | trim('a') | collect
# => 'bcd'

These do exactly the same thing. The eager form is for when you want one answer now. The pipe form is for when you want to compose.

mental model #3 β€” currying
map(add(1)) β”‚ └── add(1) is a ZefOp expecting one arg (the x) └── map is a ZefOp expecting two: a fn and a list, but you gave it only the fn β€” so it's waiting for the list to come from the left via the pipe.

So [1,2,3] | map(add(1)) is really map(add(1), [1,2,3]), which is really [add(1,1), add(1,2), add(1,3)].

pipelines are VALUES

Here's the magic moment. If I do this:

my_chain = map(add(1)) | filter(Z > 3) | reduce(add)

…that's not a function I can call. That's a value. A ZefOp chain.

print(my_chain)        # <ZefOpChain_ ...>
type(my_chain)         # ZefOpChain_

I can use it as a drop-in op in another pipeline:

[1, 2, 3, 4, 5] | my_chain | collect
# (+1 β†’) [2, 3, 4, 5, 6] β†’ (filter >3 β†’) [4, 5, 6] β†’ (sum β†’) 15
# => 15

I can pass it into a function. I can store it in a dict. I can pickle it. Pipelines are first-class values in Zef. That's a superpower.

why this matters

Saved pipelines are composable by construction. Want to build a library of reusable data transformations? Just define them as ZefOp chains. Zero boilerplate.

clean_name   = strip | to_lower_case | split(' ') | first
price_to_int = Z.replace('$', '') | int

Now they're little tools in your toolbox.

pipelines are LAZY

This is the most common source of surprise for new zef users.

[1, 2, 3] | map(add(10))
# What you expect:  [11, 12, 13]
# What you get:     <ZefOpChain_ ...>  ← a lazy pipeline!

Nothing runs until you ask. The "ask" word is collect:

[1, 2, 3] | map(add(10)) | collect
# => [11, 12, 13]  ✨

forgot | collect?

Symptom: your variable contains <ZefOpChain_ ...> or <LazyValue_ ...> instead of the answer. The fix is always the same: add | collect to the end.

Rule of thumb: if it's pure ops, end with collect. If it's an FX effect, end with run. (More on run in chapter 13.)

when does python's bitwise-or still fire?

Whenever neither side is a Zef value. For example:

5 | 3                        # 7   β€” normal Python bitwise OR
5 | add(1) | collect          # 6   β€” zef, because add(1) is a ZefOp
Int | String                   # type union, because Int & String are Zef types

wait, Int | String is a type?

Yes! Zef types also override | β€” and there it means union (the set-theoretic "or"). We'll get to that in chapter 7. For now: the | operator always means "compose these things" β€” whether they're values being fed through ops, or types being combined into unions.

composing pipelines with the other direction

You don't always want a ZefOp chain to start with data from the left. Sometimes you want to build one in isolation. That's what the curried form is for.

double_then_add_10 = map(multiply(2)) | map(add(10))

# later...
[1, 2, 3] | double_then_add_10 | collect   # => [12, 14, 16]

You can also compose chain-with-chain:

clean  = strip | to_lower_case
tokens = split(' ') | filter(Z != '')

parse = clean | tokens              # chain of chains!
'  Hello World  ' | parse | collect  # => ['hello', 'world']

mini-exercise: reshape a list

Given prices = ['$4.50', '$12', '$0.99'], build a pipeline that returns [450, 1200, 99] (cents as integers).

Hint: Z.replace('$', ''), then multiply by 100, then cast to int.

show solution
prices = ['$4.50', '$12', '$0.99']
prices
  | map(Z.replace('$', '') | float | multiply(100) | int)
  | collect
# => [450, 1200, 99]

the three ways to write the same thing

stylecodefeel
pythonsum(x+10 for x in lst if x>3)imperative, implicit
zef-eagerreduce(add, map(add(10), filter(Z>3, lst)))LISP-like, nesty
zef-pipelst | filter(Z>3) | map(add(10)) | reduce(add) | collectleft-to-right, readable

All three do the same work. The pipe form is Zef's preferred aesthetic β€” data flows from left to right, like reading English.

the golden rule

In Zef-land:

x | op  β‰‘  op(x)

And:

x | op(args)  β‰‘  op(x, args)

Data-flow arg is always first. Always.

Next up: the actual ZefOps you'll reach for every day. β†’