if you remember nothing else from this book, remember this page.
Zef's architecture fits on a napkin. All of its code falls into exactly three buckets:
These are the shapes data can take. Some you already know
(Int, String, Dict, Array).
Some are new (ET.Person, Graph, Signal,
DictDatabase).
Int, Float, String, Bool, Bytes # primitives
Array, Dict, Set, Tuple # containers
Time, PngImage, Mp3Sound # rich values
ET.Person, ET.Invoice, ET.City # your domain types
Graph, DictDatabase, Signal, Topic # structural types
You invent new types by just writing ET.Whatever. No class
definition, no registration form. The first time Zef sees ET.Dragon,
it assigns it a compact binary encoding and moves on.
ZefOps are pure functions. Pure = given the same input, always the same output, no side effects. There are about 500 shipped with Zef, all composable with the pipe operator.
map, filter, reduce, apply, match, rearrange,
sort, take, drop, group_by, join, flatten,
first, last, length, reverse, unique,
parse_json, to_json, split, trim, to_upper_case, ...
A ZefOp is a tiny machine with a data input. The pipe plugs the output of one into the input of the next:
And the whole chain โ including the data! โ is just one big value
until you | collect.
Effects are values describing something the runtime will do. Printing to a screen, calling an HTTP API, starting a server, writing a file โ all "verbs of the outside world."
FX.Print(content='hi')
FX.HTTPRequest(url='https://example.com')
FX.StartHTTPServer(routes={'/': 'hello'}, port=8000)
FX.Publish(target=some_topic, content='msg')
FX.CreateSignal(value=0)
FX.StartActor(input=topic, handler=h, initial_state=0)
Each one is data. It literally is. You can print it:
eff = FX.HTTPRequest(url='https://example.com')
print(eff) # ET.HTTPRequest(url='https://example.com')
type(eff) # Entity_
eff.url # 'https://example.com'
# nothing has happened yet! NOW it happens:
eff | run
Sort of, yes. It's the same trick Haskell's IO and Elm's
Cmd use โ defer the side effect by turning it into data.
But Zef skips the category theory vocabulary entirely: these are just ordinary
values you happen to hand to a special word (run) when you
actually want them to happen.
Here's a program that uses a type (Dict), a bunch of ZefOps (map, filter, apply), and an FX (Print):
from zef import *
people = [
{'name': 'Ada', 'age': 36},
{'name': 'Bea', 'age': 17},
{'name': 'Carl', 'age': 28},
]
# Pure pipeline โ returns a list
adults = (
people
| filter(F.age >= 18) # <- ZefOp
| map(F.name | to_upper_case) # <- ZefOp
| collect
)
# adults == ['ADA', 'CARL']
# Effect โ prints a line per adult
for name in adults:
FX.Print(content=f'๐ค {name}') | run
Look at the three sentences below. They mean different things in most languages. In Zef, they all produce the same kind of thing: a value.
That's why you can throw any of them in a list, compare them, send them over a wire, serialize them to disk. Uniformity is the feature.
A small but important detail. When Zef says "value," it really means: a chunk of memory you can memcpy.
Because of this design, a Zef value that sits in memory is the same bytes
as one sitting in a file or being sent over a socket. There's no serialization
step. When you pull a String from a database, you get back the
exact same String type you put in.
We'll see this pay off again and again โ databases don't need ORMs, IPC doesn't need JSON, graphs don't need schemas.
That's it. That's a Zef program.
If you have zef installed:
from zef import *
[1, 2, 3] | map(Z * Z) | collect # โ [1, 4, 9]
FX.Print(content='๐ฟ') | run # โ prints ๐ฟ
Feel the shape.
Next up: the pipe itself โ why it matters and how it bends Python's bitwise OR. โ