๐Ÿ 

FX: side effects as data

the single most important idea in the framework.

the usual way

In Python, "doing a thing" happens immediately:

print("hi")              # happens now
requests.get(url)         # happens now
open("f", "w").write(x)  # happens now

The side effect is baked into the function. You can't talk about it โ€” you can only trigger it.

the zef way

You don't call side-effecty functions. You construct a value that describes what to do. Nothing happens. Then you | run it to let the runtime execute it.

eff = FX.HTTPRequest(url='https://overdive.app')
print(eff)     # ET.HTTPRequest(url='...')  โ€” just a value
type(eff)      # Entity_
eff.url        # 'https://overdive.app'

response = eff | run     # NOW the HTTP call happens
mental model โ€” effects are RECIPES
FX.Print(content='hi') โ† the recipe (pure data) โ”‚ โ”‚ | run โ–ผ [runtime cook] โ”‚ โ–ผ "hi" appears on the screen โ† the actual effect

The recipe can be inspected, stored, logged, sent over a socket. Only when you | run does the runtime actually perform it.

why this pays off

the color-of-function problem, solved

Python has a "color" problem: async functions and sync functions can't freely call each other. Zef sidesteps this entirely. Everything is sync until you | run, at which point the runtime decides whether to fork a Tokio task or stay on your thread. Your code doesn't care.

the rhythm

construct โ†’ parameterize โ†’ run

example โ€” construct and run

FX.Print(content='๐ŸŒฟ') | run

example โ€” build, then fill in, then run

# only URL set โ€” body not yet filled
publisher = FX.Publish(target=my_topic)

# later...
'hello' | insert_into(publisher, 'content') | run

example โ€” batch

effects = [
    FX.Print(content='1'),
    FX.Print(content='2'),
    FX.Print(content='3'),
]

for eff in effects:
    eff | run

collect vs run

| collect

Runs a pure ZefOp pipeline. Returns the result.

[1,2] | map(add(1)) | collect
| run

Executes an effect value. Returns whatever the effect produces.

FX.HTTPRequest(url='...') | run

An effect can return a result (like FX.HTTPRequest returning the response). That's why | run has a return value.

inspecting effects

Since effects are data, you can look at them:

eff = FX.HTTPRequest(
    url='https://api.example.com',
    method='POST',
    body='{"hi":1}',
)

eff.url          # 'https://api.example.com'
eff.method       # 'POST'
type(eff)       # Entity_

# you can even write them to disk as .zef files
FX.SaveToLocalFile(content=[eff], path='/tmp/plan.zef', overwrite_existing=True) | run

# and load them back later
plan = FX.LoadFromLocalFile(path='/tmp/plan.zef') | run
for eff in plan:
    eff | run

writing pure functions that RETURN effects

This is the pattern you'll use the most. Your logic is pure; your function returns an effect (or a list of effects); your caller runs them.

def welcome(name):
    return [
        FX.Print(content=f'Welcome, {name}!'),
        FX.Log(content=ET.UserJoined(name=name)),
    ]

# PURE function โ€” nothing happens
plan = welcome('Alice')

# ACT โ€” run at the boundary
for eff in plan:
    eff | run

"functional core, imperative shell"

You've just implemented Gary Bernhardt's famous pattern. The core of your app is a pure function producing data (effects); the shell runs them. This is the structure of every serious Zef program.

inside pipelines: insert_into

Often you want to build part of an effect ahead of time, and fill the rest from a pipeline. insert_into makes the data-flow arg go into a specific field:

publisher = FX.Publish(target=chat_topic)       # content missing

'hello' | insert_into(publisher, 'content') | run
# equivalent to FX.Publish(target=chat_topic, content='hello') | run

Great for setups like FX.SubscribeFX(topic=..., op=...) where the subscriber's op chain needs to take incoming messages and push them into another effect.

the whole idea on one card

in zef, side effects are values.
you build them; nothing happens.
you | run them; things happen.
pure code returns effects; shells execute them.

a tiny reflection

Which of the following does NOT require | run?

  1. FX.Print(content='hi')
  2. [1,2,3] | reduce(add)
  3. FX.CreateSignal(value=0)
answer

#1 and #3 are effects โ€” they need | run. #2 is pure โ€” you just need | collect. But #1 and #3 without | run are simply inert values; they won't print/create anything until you ask.

Next up: a tour of the FX catalog โ€” what's in the box. โ†’