polar

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.