the single most important idea in the framework.
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.
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
The recipe can be inspected, stored, logged, sent over a socket. Only when
you | run does the runtime actually perform it.
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.
FX.Print(content='๐ฟ') | run
# only URL set โ body not yet filled
publisher = FX.Publish(target=my_topic)
# later...
'hello' | insert_into(publisher, 'content') | run
effects = [
FX.Print(content='1'),
FX.Print(content='2'),
FX.Print(content='3'),
]
for eff in effects:
eff | run
collect vs runRuns a pure ZefOp pipeline. Returns the result.
[1,2] | map(add(1)) | collect
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.
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
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
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.
insert_intoOften 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.
| run them; things happen.Which of the following does NOT require | run?
FX.Print(content='hi')[1,2,3] | reduce(add)FX.CreateSignal(value=0)#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. โ