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, throughMut. -
Table.inserttakes a function from the new id to the row, so the row can contain its own id. -
Table.findreturns anOption, andTable.updateandTable.deletereturn 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.