Mutation and copy-on-write#
type Counter {
value: Int! = 0
incr: Counter! {
value += 1 # bare field write forks the receiver; `self.value += 1` is the equivalent explicit form
self
}
# a method can recurse: the naked `incrBy` call forks self just like `self.incrBy` would
incrBy(n: Int!): Counter! { if (n <= 0) { self } else { incr.incrBy(n - 1) } }
}
# a "mutating" method returns a fork — the original is untouched
let answer = Counter(42)
[answer.incr.value, answer.value]
=> [43, 42]
# recursion forks per call too: it accumulates into the returned fork and
# leaves the original untouched
let start = Counter(0)
[start.incrBy(3).value, start.value]
=> [3, 0]
type Image {
packages: [String!]! = []
# chainable methods return the concrete type name — there is no `Self` keyword
with(pkg: String!): Image! {
packages += [pkg]
self
}
withAll(pkgs: [String!]!): Image! {
pkgs.each { p => packages += [p] } # bare field write works inside a closure too
self
}
}
# each call forks from the receiver, so forks of one base never compound,
# and `base` itself is never modified
let base = Image
[base.with("git").packages, base.with("curl").packages, base.packages]
=> [["git"], ["curl"], []]
# within a single call, writes accumulate onto one fork
Image.withAll(["git", "curl", "tini"]).packages
=> ["git", "curl", "tini"]
# pure functions have no `self`, so nothing forks — they just compute
double(x: Int!): Int! { x * 2 }
double(21)
=> 42
# plain bindings are not copy-on-write: a bare write to a local just rebinds it
let n = 1
n = 2
n
=> 2
type Greeting {
text: String!
new(text: String!) {
# `self.` is needed only to disambiguate from a same-named arg/local;
# bare `text = ...` would rebind the arg, not the field
self.text = "hello, " + text
self
}
}
Greeting("world").text
=> "hello, world"
type Leaf { c: Int! }
type Node { leaf: Leaf! }
type Tree {
node: Node!
# assignment through a path forks each step; subtrees off the path are shared
bump: Tree! { self.node.leaf.c += 100; self }
}
# the original is untouched at any depth
let tree = Tree(Node(Leaf(42)))
[tree.bump.node.leaf.c, tree.node.leaf.c]
=> [142, 42]
# a loop threads one shared accumulator: writes are visible to the next
# iteration and outlive the loop
let total = 0
[1, 2, 3].each { x => total += x }
total
=> 6
# every `{{ }}` evaluates its fields concurrently, each in its own fork:
# `b` can't see `a`'s write, and the outer `counter` stays untouched
let counter = 0
let pair = {{
a: { counter += 1; counter },
b: { counter += 10; counter }
}}
[pair.a, pair.b, counter]
=> [1, 10, 0]
# fields coordinate by value in any order (a field naming a sibling waits for it)
{{ total: a + b, a: 1, b: 2 }}.total
=> 3
# a genuine cycle between fields is a compile error
{{ x: y, y: x }}
Type error: object literal has cyclic field dependencies: x -> y -> x
# concurrent fields fail fast, but the reported error is always the
# lowest in source order — never whichever lost the race
{{ a: raise "first", b: raise "second" }}
Runtime error: first
[1m[31malso failed:[0m error: second [2m(:3:25)[0m
# deterministic even when the lower field finishes last:
# `slow` (field 0) surfaces though `fast` raises first
{{
slow: { [1, 2, 3, 4].each { _ => 0 }; raise "from slow" },
fast: raise "from fast"
}}
Runtime error: from slow
[1m[31malso failed:[0m error: from fast [2m(:5:9)[0m
# a selection over a list fans out across its elements (also concurrent);
# the result is a list of records, compared by value
type User { name: String! }
[User("Alice"), User("Bob")].{{ name }} == [{{ name: "Alice" }}, {{ name: "Bob" }}]
=> true
# over a GraphQL receiver the same `{{ }}` concurrency is batched I/O:
# a multi-field selection is one round trip; a record of selections runs its
# queries in parallel
{{ users: users.{{ name }}, repos: repos.{{ name }} }}