polar

Mutable State

Most Polar code never changes a value: it makes new ones. When you do need something that changes over time, like a counter or an in-memory store, Polar gives you mutable cells, tracked by the Mut effect.

Ref: a single mutable cell

Std.Ref provides a box holding one value that can be replaced:

uses
  Std.Ref

functions
  count_clicks() -> Int / {Mut} {
    let clicks = Ref.new(0)

    Ref.update(clicks, function(n) { n + 1 })
    Ref.update(clicks, function(n) { n + 1 })
    Ref.get(clicks)
  }
Function Does
Ref.new(value) Makes a new cell. Creating one is pure
Ref.get(ref) Reads the current value
Ref.set(ref, value) Replaces it
Ref.update(ref, f) Replaces it with f(current)
Ref.modify(ref, f) Like update, but f returns { value, result }: the new value, plus something to hand back

The Mut effect

Reading or writing a cell uses the Mut effect, so any function that does it says / {Mut}. You can always tell from a signature whether a function might depend on, or change, mutable state.

Note: Mut is available on every host. It doesn't limit where code runs. It only makes the mutation visible in the type.

Table: an in-memory store

Std.Table is a small in-memory database built on Ref. Rows get numeric ids automatically. It's useful for prototypes, tests and caches:

module State

uses
  Std.List
  Std.Option
  Std.Ref
  Std.Table

types
  Todo = { id: Int, title: String, done: Bool }

constants
  todos: Table<Todo> = Table.new()

functions
  add(title: String) -> Todo / {Mut} {
    Table.insert(todos, function(id) { { id: id, title: title, done: false } })
  }

  finish(id: Int) -> Bool / {Mut} {
    match Table.find(todos, id) {
      Some(t) -> Table.update(todos, id, { ..t, done: true }),
      None -> false,
    }
  }

  main() -> {} / {Mut} {
    let clicks = Ref.new(0)

    Ref.update(clicks, function(n) { n + 1 })
    Ref.update(clicks, function(n) { n + 1 })
    Log.info("clicked #{Ref.get(clicks)} times")

    add("write docs")
    add("ship it")
    finish(1)

    let lines = List.map(
      Table.all(todos),
      function(t) { "#{t.id}. #{t.title}#{if t.done { " (done)" } else { "" }}" },
    )

    Log.info(List.join(lines, "\n"))
    Log.info("#{Table.size(todos)} todos")
  }

exports
  main
$ polar run state.px
clicked 2 times
1. write docs (done)
2. ship it
2 todos

A few things to notice:

  • The table is a constant. The constant itself never changes: it always refers to the same table. What changes is the data inside, through Mut.
  • Table.insert takes a function from the new id to the row, so the row can contain its own id.
  • Table.find returns an Option, and Table.update and Table.delete return whether the row existed.

Tip: Keep mutation in a few small functions, like add and finish above, and keep the rest of your code pure. The / {Mut} in signatures makes it easy to check you've done that.