๐Ÿ 

@zef_function

your python function, in the zef universe.

the problem

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.

the one-line solution

@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!'

what actually happens at decoration time

When Zef sees @zef_function, a lot of machinery fires:

  1. The function's source is extracted
  2. Any captured variables are snapshotted
  3. Source + captures are hashed โ†’ a content UID
  4. An entity describing the function is added to the code graph
  5. A ZefOp is returned โ€” callable like any other ZefOp
mental model โ€” the function is now a VALUE

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.

why content addressability matters

multiple methods, one function

Like 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.

refinement types in signatures

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']

what you can capture

safe to capture
  • primitives (int, str, float, bool)
  • literal lists, dicts, sets
  • other @zef_functions at module level
  • zef values (signals, topics, db handles)
NOT safe to capture
  • plain Python helpers (def f():)
  • module imports / aliases
  • classes, complex objects
  • anything you don't want frozen

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) + '!'

using it in FX contexts

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.

are zef functions always pure?

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.

the guideline

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 fully worked tiny example

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

practice

Write two @zef_functions:

  1. is_prime โ€” takes an Int, returns Bool
  2. primes_below โ€” takes an Int n, returns the primes < n (uses is_prime)
solution
@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. โ†’