your python function, in the zef universe.
You want to write a chunk of logic in regular Python (because classes, for
loops, list comprehensions โ whatever). But you also want it to play nice with
Zef: usable inside FX.SubscribeFX(op=...), as an actor handler,
as a ZefOp in a pipeline.
@zef_function
def shouty(msg: String) -> String:
return msg.upper() + '!'
Now shouty is a ZefOp. Use it anywhere a ZefOp goes:
['hi', 'there'] | map(shouty) | collect # ['HI!', 'THERE!']
'zef' | shouty | collect # 'ZEF!'
When Zef sees @zef_function, a lot of machinery fires:
A Python function is a closure โ mutable, context-dependent, hard to serialize.
A @zef_function is a value: source text + captured data, frozen
at decoration. Given the same source and captured values, you get the same
content UID. Everywhere. Across machines. Across time.
This is called "content addressability" and it's inspired by the Unison language.
@zef_function over a socket; the receiver has enough info to run itLike Julia or Elixir, Zef functions support multiple dispatch. Define several methods for the same name; the engine picks by input signature:
@zef_function
def describe(x: Int) -> String:
return f'an integer: {x}'
@zef_function
def describe(x: String) -> String:
return f'a string: "{x}"'
@zef_function
def describe(x: Array) -> String:
return f'a list of {len(x)} things'
42 | describe | collect # 'an integer: 42'
'hi' | describe | collect # 'a string: "hi"'
[1,2,3] | describe | collect # 'a list of 3 things'
Methods are checked top-to-bottom. The first one whose type signature matches the args is called.
Because types are sets and refinement is free, your method signatures can be as specific as you want:
@zef_function
def status(age: Int & (Z < 18)) -> String:
return 'minor'
@zef_function
def status(age: Int & (Z >= 18) & (Z < 65)) -> String:
return 'adult'
@zef_function
def status(age: Int & (Z >= 65)) -> String:
return 'senior'
[10, 30, 70] | map(status) | collect
# ['minor', 'adult', 'senior']
@zef_functions at module leveldef f():)If you need a helper, either define it inside the function body or decorate
the helper itself with @zef_function.
# โ risky โ plain helper
def clean(s): return s.strip().lower()
@zef_function
def process(s: String) -> String:
return clean(s) + '!'
# โ
safer โ inline helper
@zef_function
def process(s: String) -> String:
def clean(x): return x.strip().lower()
return clean(s) + '!'
# โ
safest โ zef-function helper
@zef_function
def clean(s: String) -> String: return s.strip().lower()
@zef_function
def process(s: String) -> String:
return (s | clean | collect) + '!'
The real payoff โ anywhere a ZefOp goes, a @zef_function goes:
@zef_function
def on_message(msg: String) -> Any:
print(f'got: {msg}')
return FX.Publish(target=other_topic, content=msg + ' (echoed)')
# in a subscriber
FX.SubscribeFX(topic=my_topic, op=on_message) | run
# in an actor (see ch 18)
FX.StartActor(input=topic, initial_state=0, handler=on_message) | run
If the function returns an effect, it'll be automatically | run.
Ideally, yes. Practically, Zef is pragmatic โ you can use side-effectful
Python inside a @zef_function when you need to, with the
understanding that caching and reproducibility assume purity.
Write like it's pure. If the function takes and returns data, it's a good
Zef function. If it needs I/O, return an FX (or a list of FX) and let the
caller | run โ same pattern we saw in chapter 13.
A simple data-cleaning pipeline expressed as zef functions:
@zef_function
def parse_user(raw: String) -> Dict:
name, age = raw.split(',')
return {'name': name.strip(), 'age': int(age)}
@zef_function
def to_entity(d: Dict) -> Any:
return ET.User(name=d['name'], age=d['age'])
lines = ['Alice, 30', 'Bob,25', 'Carol,42']
users = lines | map(parse_user | to_entity) | collect
Write two @zef_functions:
is_prime โ takes an Int, returns Boolprimes_below โ takes an Int n, returns the primes < n (uses is_prime)@zef_function
def is_prime(n: Int) -> Bool:
if n < 2: return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0: return False
return True
@zef_function
def primes_below(n: Int) -> Array:
return [i for i in range(n) if is_prime(i, ) | collect]
30 | primes_below | collect
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Next up: databases โ DictDB, ArrayDB, and the file-backed variants. โ