|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.
This is a small but important rule. Every ZefOp accepts its data-flow argument first, and the other args curried:
trim('abcd', 'a')
# => 'bcd'
'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.
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)].
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.
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.
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] β¨
| 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.)
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
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.
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']
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.
prices = ['$4.50', '$12', '$0.99']
prices
| map(Z.replace('$', '') | float | multiply(100) | int)
| collect
# => [450, 1200, 99]
| style | code | feel |
|---|---|---|
| python | sum(x+10 for x in lst if x>3) | imperative, implicit |
| zef-eager | reduce(add, map(add(10), filter(Z>3, lst))) | LISP-like, nesty |
| zef-pipe | lst | filter(Z>3) | map(add(10)) | reduce(add) | collect | left-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.
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. β