Effects
An effect is anything a function does besides computing a value: reading the clock, writing to the DOM, talking to a database, running a process. In Polar, effects are part of a function's type.
Why put effects in the type?
In JavaScript, any function can do anything.
formatPrice(x) might also send a network request, and
the only way to know is to read it, and everything it calls.
In Polar, you can tell from the signature. A function either lists the effects it uses, or it's pure. The compiler checks both directions: a pure function can't sneak in an effect, and a function can't use an effect it didn't declare. That makes code easier to test, easier to move around, and lets Polar decide where code can run.
Declaring an effect
Effects go in the effects zone. An effect has a name,
the host it lives on, and its operations:
hosts
Node
effects
Clock in Node {
now() -> Int
}
This declares a Clock effect, available on
Node, with one operation, now. It doesn't
say how to get the time yet. That comes later, in a bind.
Using an effect
Call an operation through the effect's name. A function that does this lists the effect after its return type, following a slash:
stamp(label: String) -> String / {Clock} {
"#{label} at #{Clock.now()}"
}
Read -> String / {Clock} as "returns a String, and
uses the Clock". A function can list several:
/ {Dom, Clock}.
Pure means pure
A function written with -> and no
/ {…} promises to use no effects. Here's what happens
if it breaks that promise:
double(n: Int) -> Int {
n * Clock.now()
}
error[POLAR0808]: `double` uses `Clock`, but its signature says it is pure
--> clock.px:17:9
|
17 | n * Clock.now()
| ^^^^^^^^^^^ this uses `Clock`
|
= note: a signature with `->` and no `/ {…}` promises no effects
= help: add `/ {Clock}` after the return type
Effects also travel up. If report calls
stamp, then report uses
Clock too, and has to say so.
Note: A function with no return type at
all, like main(), gets its effects inferred along
with its return type. That's handy for small programs and for
main, but writing signatures on everything else
documents what your code does.
Binding an effect
A declaration says what operations exist. A
bind says how they work on a particular host. Binds
go in the binds zone:
binds
Clock in Node {
now() {
1700000000
}
}
This bind is written in Polar and always returns the same time,
which is handy in tests. A real program would usually bind
Clock to JavaScript instead. See
JavaScript Interop.
Separating the declaration from the bind is what makes effects
useful. Code that uses Clock doesn't care where the
time comes from, so you can swap the bind without touching it.
Effects that pass through
What about a function like List.map? It's pure itself,
but the function you give it might not be. Its signature handles
both:
map(xs: List<a>, f: function(a) -> b / {| e}) -> List<b> / {| e}
{| e} means "whatever effects f has".
Given a pure function, List.map is pure. Given
stamp, it uses Clock. Write your own
higher-order functions the same way, and they'll work with any
callback.
All together
module Clock
uses
Std.List
hosts
Node
effects
Clock in Node {
now() -> Int
}
binds
Clock in Node {
now() {
1700000000
}
}
functions
stamp(label: String) -> String / {Clock} {
"#{label} at #{Clock.now()}"
}
double(n: Int) -> Int {
n * 2
}
main() -> {} / {Clock} {
let labels = List.map(["start", "end"], stamp)
Log.info(List.join(labels, "\n"))
Log.info("#{double(21)}")
}
exports
main
$ polar run clock.px
start at 1700000000
end at 1700000000
42
Errors are effects too, and they have some syntax of their own. That's next: Handling Errors.