plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


3e4d318 — Peter John 2026-11-03T15:31:42+05:30
examples: functional.plum operations as enum methods; loops section
Files changed (1) hide show
  1. plum-examples/functional.plum +314 -241
plum-examples/functional.plum CHANGED
@@ -4,16 +4,19 @@
4
4
  # of recursive sum types (`enum`) whose variants are declared inline with named
5
5
  # fields, `Cons(head: T, tail: Link[T])`. A variant is a constructor and a
6
6
  # pattern only; `Link[T]` is the only type, and fields are read by matching.
7
- # Every operation is a recursive function whose body is `= match x` with the
7
+ # Every operation is a method on its enum, a recursive function whose body is
8
- # arms indented below, returning a new value. Unchanged parts are shared.
8
+ # `= match self` with the arms indented below, returning a new value. Unchanged
9
+ # parts are shared.
9
10
  #
10
- # 1. Link[T]: a cons list (`Nil` or `Cons(head, tail)`) and its combinators
11
+ # 1. Link[T]: a cons list (`Nil` or `Cons(head, tail)`), combinators as methods on the enum
11
12
  # 2. Functions as values: closures, composition, currying, pipelines
12
- # 3. Tree[T]: a persistent binary search tree
13
+ # 3. Tree: a persistent binary search tree, operations as methods on the enum
13
14
  # 4. Pattern matching: nested patterns, guards, pattern `if`, optionals
14
15
  # 5. A pure scene graph: a recursive `Node` tree whose groups hold a
15
16
  # `Link[Node]` of children; acting, drawing, hit testing and event routing
16
- # are all functions from an old tree to a new one
17
+ # are all methods from an old tree to a new one
18
+ #
19
+ # 6. Loops, functionally: the same job as methods, a `for` and a `while`
17
20
  #
18
21
  # Recursion is not tail call optimised yet, so very long lists (hundreds of
19
22
  # thousands of elements) would overflow the stack. Scene trees are shallow.
@@ -21,13 +24,92 @@ module functional
21
24
  import std/Str
22
25
  import std/Int
23
26
  import std/Float
27
+ import std/List
24
28
 
25
29
  # ---------------------------------------------------------------------------
26
30
  # 1. Link[T]: a cons list
27
31
  # ---------------------------------------------------------------------------
28
32
 
29
33
  # A list is either empty (`Nil`, a tag) or a head value in front of another list.
34
+ # Its operations are methods on the enum.
35
+ enum Link[T] =
36
+ | Nil
30
- enum Link[T] = Nil | Cons(head: T, tail: Link[T])
37
+ | Cons(head: T, tail: Link[T])
38
+
39
+ fun length(self) -> Int = match self
40
+ Nil => 0
41
+ Cons(_, t) => 1 + t.length()
42
+
43
+ fun isEmpty(self) -> Bool = match self
44
+ Nil => true
45
+ Cons(_, _) => false
46
+ # Runs `f` on every element, in order, for its effect.
47
+ fun each(self, f: fn(T)) -> Unit =
48
+ if Cons(h, t) := self
49
+ f(h)
50
+ t.each(f)
51
+ # A `for` source: walks the list with a local cursor and yields each head.
52
+ fun* items(self) *T =
53
+ cur := self
54
+ while true
55
+ if Cons(h, t) := cur
56
+ cur = t
57
+ yield h
58
+ else
59
+ break
60
+ # Structural recursion: rebuild the list with `f` applied to every element.
61
+ fun map(self, f: fn(T) -> B) -> Link[B] = match self
62
+ Nil => Nil
63
+ Cons(h, t) => Cons(head: f(h), tail: t.map(f))
64
+
65
+ fun filter(self, keep: fn(T) -> Bool) -> Link[T] = match self
66
+ Nil => Nil
67
+ Cons(h, t) =>
68
+ rest := t.filter(keep)
69
+ if keep(h) then cons(h, rest) else rest
70
+ # Left fold: `f(f(f(acc, x0), x1), x2)`. Tail recursive in shape.
71
+ fun foldl(self, acc: A, f: fn(A, T) -> A) -> A = match self
72
+ Nil => acc
73
+ Cons(h, t) => t.foldl(f(acc, h), f)
74
+ # Right fold: `f(x0, f(x1, f(x2, acc)))`. Keeps the list's order when rebuilding.
75
+ fun foldr(self, acc: A, f: fn(T, A) -> A) -> A = match self
76
+ Nil => acc
77
+ Cons(h, t) => f(h, t.foldr(acc, f))
78
+
79
+ fun reverse(self) -> Link[T] =
80
+ self.foldl(Nil, |acc, x| cons(x, acc))
81
+ # `self` followed by `ys`. Only `self` is rebuilt; `ys` is shared.
82
+ fun append(self, ys: Link[T]) -> Link[T] =
83
+ self.foldr(ys, |x, acc| cons(x, acc))
84
+
85
+ fun any(self, p: fn(T) -> Bool) -> Bool = match self
86
+ Nil => false
87
+ Cons(h, t) => p(h) || t.any(p)
88
+
89
+ fun all(self, p: fn(T) -> Bool) -> Bool = match self
90
+ Nil => true
91
+ Cons(h, t) => p(h) && t.all(p)
92
+
93
+ fun take(self, n: Int) -> Link[T] = match self
94
+ Nil => Nil
95
+ Cons(h, t) => if n <= 0 then Nil else cons(h, t.take(n - 1))
96
+
97
+ fun drop(self, n: Int) -> Link[T] = match self
98
+ Nil => Nil
99
+ Cons(_, t) => if n <= 0 then self else t.drop(n - 1)
100
+ # The first element, or null for an empty list.
101
+ fun first(self) -> T? = match self
102
+ Nil => null
103
+ Cons(h, _) => h
104
+ # Pairs up two lists, stopping at the shorter one.
105
+ fun zip(self, ys: Link[B]) -> Link[Pair[T, B]] = match self
106
+ Nil => Nil
107
+ Cons(x, xt) =>
108
+ match ys
109
+ Nil => Nil
110
+ Cons(y, yt) => cons(Pair[T, B](fst: x, snd: y), xt.zip(yt))
111
+
112
+ record Pair[A, B](fst: A, snd: B)
31
113
 
32
114
  # Prepends `h`: O(1), and `t` is shared, not copied.
33
115
  fun cons(h: T, t: Link[T]) -> Link[T] = Cons(head: h, tail: t)
@@ -38,75 +120,8 @@ fun single(x: T) -> Link[T] = cons(x, Nil)
38
120
  fun fromTo(a: Int, b: Int) -> Link[Int] =
39
121
  if a >= b then Nil else cons(a, fromTo(a + 1, b))
40
122
 
41
- fun length(xs: Link[T]) -> Int = match xs
42
- Nil => 0
43
- Cons(_, t) => 1 + length(t)
44
-
45
- fun isEmpty(xs: Link[T]) -> Bool = match xs
46
- Nil => true
47
- Cons(_, _) => false
48
-
49
- # Structural recursion: rebuild the list with `f` applied to every element.
50
- fun map(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
51
- Nil => Nil
52
- Cons(h, t) => Cons(head: f(h), tail: map(t, f))
53
-
54
- fun filter(xs: Link[T], keep: fn(T) -> Bool) -> Link[T] = match xs
55
- Nil => Nil
56
- Cons(h, t) =>
57
- rest := filter(t, keep)
58
- if keep(h) then cons(h, rest) else rest
59
-
60
- # Left fold: `f(f(f(acc, x0), x1), x2)`. Tail recursive in shape.
61
- fun foldl(xs: Link[T], acc: A, f: fn(A, T) -> A) -> A = match xs
62
- Nil => acc
63
- Cons(h, t) => foldl(t, f(acc, h), f)
64
-
65
- # Right fold: `f(x0, f(x1, f(x2, acc)))`. Keeps the list's order when rebuilding.
66
- fun foldr(xs: Link[T], acc: A, f: fn(T, A) -> A) -> A = match xs
67
- Nil => acc
68
- Cons(h, t) => f(h, foldr(t, acc, f))
69
-
70
- fun reverse(xs: Link[T]) -> Link[T] =
71
- foldl(xs, Nil, |acc, x| cons(x, acc))
72
-
73
- # `xs` followed by `ys`. Only `xs` is rebuilt; `ys` is shared.
74
- fun append(xs: Link[T], ys: Link[T]) -> Link[T] =
75
- foldr(xs, ys, |x, acc| cons(x, acc))
76
-
77
123
  fun sum(xs: Link[Int]) -> Int =
78
- foldl(xs, 0, |a, x| a + x)
124
+ xs.foldl(0, |a, x| a + x)
79
-
80
- fun any(xs: Link[T], p: fn(T) -> Bool) -> Bool = match xs
81
- Nil => false
82
- Cons(h, t) => p(h) || any(t, p)
83
-
84
- fun all(xs: Link[T], p: fn(T) -> Bool) -> Bool = match xs
85
- Nil => true
86
- Cons(h, t) => p(h) && all(t, p)
87
-
88
- fun take(xs: Link[T], n: Int) -> Link[T] = match xs
89
- Nil => Nil
90
- Cons(h, t) => if n <= 0 then Nil else cons(h, take(t, n - 1))
91
-
92
- fun drop(xs: Link[T], n: Int) -> Link[T] = match xs
93
- Nil => Nil
94
- Cons(_, t) => if n <= 0 then xs else drop(t, n - 1)
95
-
96
- # The first element, or null for an empty list.
97
- fun first(xs: Link[T]) -> T? = match xs
98
- Nil => null
99
- Cons(h, _) => h
100
-
101
- # Pairs up two lists, stopping at the shorter one.
102
- record Pair[A, B](fst: A, snd: B)
103
-
104
- fun zip(xs: Link[A], ys: Link[B]) -> Link[Pair[A, B]] = match xs
105
- Nil => Nil
106
- Cons(x, xt) =>
107
- match ys
108
- Nil => Nil
109
- Cons(y, yt) => cons(Pair[A, B](fst: x, snd: y), zip(xt, yt))
110
125
 
111
126
  fun joinInts(xs: Link[Int], sep: Str) -> Str = match xs
112
127
  Nil => ""
@@ -144,62 +159,65 @@ fun iterate(f: fn(T) -> T, n: Int, x: T) -> T =
144
159
  # ---------------------------------------------------------------------------
145
160
 
146
161
  # `insert` returns a new tree that shares every subtree it did not walk through:
147
- # inserting into a tree of n nodes allocates about log n new branches.
162
+ # inserting into a tree of n nodes allocates about log n new branches. Its
163
+ # operations are methods on the enum.
164
+ enum Tree =
165
+ | Leaf
148
- enum Tree = Leaf | Branch(left: Tree, value: Int, right: Tree)
166
+ | Branch(left: Tree, value: Int, right: Tree)
149
-
167
+
150
- fun insert(t: Tree, v: Int) -> Tree = match t
168
+ fun insert(self, v: Int) -> Tree = match self
151
- Leaf => Branch(left: Leaf, value: v, right: Leaf)
169
+ Leaf => Branch(left: Leaf, value: v, right: Leaf)
152
- Branch(l, x, r) =>
170
+ Branch(l, x, r) =>
153
- if v < x
171
+ if v < x
154
- Branch(left: insert(l, v), value: x, right: r)
172
+ Branch(left: l.insert(v), value: x, right: r)
155
- else if v > x
173
+ else if v > x
156
- Branch(left: l, value: x, right: insert(r, v))
174
+ Branch(left: l, value: x, right: r.insert(v))
157
- else
175
+ else
158
- t
176
+ self
177
+
178
+ fun member(self, v: Int) -> Bool = match self
179
+ Leaf => false
180
+ Branch(l, x, r) => if v < x then l.member(v) else if v > x then r.member(v) else true
181
+
182
+ fun size(self) -> Int = match self
183
+ Leaf => 0
184
+ Branch(l, _, r) => l.size() + 1 + r.size()
185
+
186
+ fun depth(self) -> Int = match self
187
+ Leaf => 0
188
+ Branch(l, _, r) => 1 + l.depth().max(r.depth())
189
+ # In-order traversal as a fold, so `inOrder` comes out sorted.
190
+ fun fold(self, acc: A, f: fn(Int, A) -> A) -> A = match self
191
+ Leaf => acc
192
+ Branch(l, x, r) => l.fold(f(x, r.fold(acc, f)), f)
193
+
194
+ fun inOrder(self) -> Link[Int] =
195
+ self.fold(Nil, |x, acc| cons(x, acc))
196
+
197
+ fun smallest(self) -> Int? = match self
198
+ Leaf => null
199
+ Branch(Leaf, x, _) => x
200
+ Branch(l, _, _) => l.smallest()
159
201
 
160
202
  fun fromLink(xs: Link[Int]) -> Tree =
161
- foldl(xs, Leaf, |t, x| insert(t, x))
203
+ xs.foldl(Leaf, |t, x| t.insert(x))
162
-
163
- fun member(t: Tree, v: Int) -> Bool = match t
164
- Leaf => false
165
- Branch(l, x, r) => if v < x then member(l, v) else if v > x then member(r, v) else true
166
-
167
- fun size(t: Tree) -> Int = match t
168
- Leaf => 0
169
- Branch(l, _, r) => size(l) + 1 + size(r)
170
-
171
- fun depth(t: Tree) -> Int = match t
172
- Leaf => 0
173
- Branch(l, _, r) => 1 + depth(l).max(depth(r))
174
-
175
- # In-order traversal as a fold, so `inOrder` comes out sorted.
176
- fun foldTree(t: Tree, acc: A, f: fn(Int, A) -> A) -> A = match t
177
- Leaf => acc
178
- Branch(l, x, r) => foldTree(l, f(x, foldTree(r, acc, f)), f)
179
-
180
- fun inOrder(t: Tree) -> Link[Int] =
181
- foldTree(t, Nil, |x, acc| cons(x, acc))
182
-
183
- fun smallest(t: Tree) -> Int? = match t
184
- Leaf => null
185
- Branch(Leaf, x, _) => x
186
- Branch(l, _, _) => smallest(l)
187
204
 
188
205
  # ---------------------------------------------------------------------------
189
206
  # 4. Pattern matching
190
207
  # ---------------------------------------------------------------------------
191
208
 
209
+ enum Shape =
210
+ | Circle(r: Float)
192
- enum Shape = Circle(r: Float) | Rect(w: Float, h: Float)
211
+ | Rect(w: Float, h: Float)
193
-
194
- fun describe(s: Shape) -> Str = match s
195
- Circle(r) when r == 0.0 => "point"
196
- Circle(_) => "circle"
197
- Rect(w, h) when w == h => "square"
198
- Rect(_, _) => "rect"
199
212
 
213
+ fun describe(self) -> Str = match self
214
+ Circle(r) when r == 0.0 => "point"
215
+ Circle(_) => "circle"
216
+ Rect(w, h) when w == h => "square"
217
+ Rect(_, _) => "rect"
200
- # Pattern `if` with a guard after the comma.
218
+ # Pattern `if` with a guard after the comma.
201
- fun wide(s: Shape) -> Bool =
219
+ fun wide(self) -> Bool =
202
- if Rect(w, h) := s, w > h then true else false
220
+ if Rect(w, h) := self, w > h then true else false
203
221
 
204
222
  # Nested patterns look two levels into a list.
205
223
  fun firstTwo(xs: Link[Int]) -> Str = match xs
@@ -226,47 +244,8 @@ fun undo(xf: Xf, px: Float, py: Float) -> Pt =
226
244
 
227
245
  record Pt(x: Float, y: Float)
228
246
 
229
- # The node kinds, declared inline. Every node has an `id` and a local transform
230
- # `xf`. A Group's children are a cons list of nodes: the tree is a recursive type
231
- # all the way down.
232
- enum Node =
233
- | Sprite(id: Int, xf: Xf, w: Float, h: Float, image: Str, spin: Float)
234
- | Label(id: Int, xf: Xf, text: Str)
235
- | Button(id: Int, xf: Xf, w: Float, h: Float, text: Str, pressed: Bool)
236
- | Group(id: Int, xf: Xf, children: Link[Node])
237
-
238
- fun nodeId(n: Node) -> Int = match n
239
- Sprite(id, _, _, _, _, _) => id
240
- Label(id, _, _) => id
241
- Button(id, _, _, _, _, _) => id
242
- Group(id, _, _) => id
243
-
244
- fun nodeXf(n: Node) -> Xf = match n
245
- Sprite(_, xf, _, _, _, _) => xf
246
- Label(_, xf, _) => xf
247
- Button(_, xf, _, _, _, _) => xf
248
- Group(_, xf, _) => xf
249
-
250
- # The number of nodes in the tree.
251
- fun count(n: Node) -> Int = match n
252
- Group(_, _, kids) => 1 + foldl(kids, 0, |a, k| a + count(k))
253
- _ => 1
254
-
255
- # A generic bottom-up rewrite: rebuild the children first, then apply `f`.
256
- fun mapNodes(n: Node, f: fn(Node) -> Node) -> Node = match n
257
- Group(id, xf, kids) => f(Group(id: id, xf: xf, children: map(kids, |k| mapNodes(k, f))))
258
- _ => f(n)
259
-
260
- # Replaces the node with id `target` by `f(node)`; every other node is kept.
261
- fun update(n: Node, target: Int, f: fn(Node) -> Node) -> Node =
262
- mapNodes(n, |m| if nodeId(m) == target then f(m) else m)
263
-
264
- # One step of behaviour: a sprite moves by its `spin` per second to the right.
265
- # Returns a new tree; the old one is untouched.
266
- fun act(n: Node, dt: Float) -> Node = match n
247
+ fun inBox(p: Pt, w: Float, h: Float) -> Bool =
267
- Sprite(id, xf, w, h, image, spin) => Sprite(id: id, xf: Xf(x: xf.x + spin * dt, y: xf.y, s: xf.s), w: w, h: h, image: image, spin: spin)
268
- Group(gid, gxf, kids) => Group(id: gid, xf: gxf, children: map(kids, |k| act(k, dt)))
248
+ p.x >= 0.0 && p.x <= w && p.y >= 0.0 && p.y <= h
269
- _ => n
270
249
 
271
250
  # What drawing would paint, as data: a pure list of commands for a host to run.
272
251
  record DrawCmd(kind: Str, id: Int, x: Float, y: Float, s: Float)
@@ -274,38 +253,75 @@ record DrawCmd(kind: Str, id: Int, x: Float, y: Float, s: Float)
274
253
  fun cmd(kind: Str, id: Int, xf: Xf) -> DrawCmd =
275
254
  DrawCmd(kind: kind, id: id, x: xf.x, y: xf.y, s: xf.s)
276
255
 
277
- # Depth first, parents before children, so later commands draw on top.
256
+ # The node kinds, declared inline. Every node has an `id` and a local transform
257
+ # `xf`. A Group's children are a cons list of nodes: the tree is a recursive type
278
- fun draw(n: Node, parent: Xf) -> Link[DrawCmd] =
258
+ # all the way down. Its operations are methods on the enum.
279
- xf := thenXf(parent, nodeXf(n))
280
- match n
259
+ enum Node =
281
- Sprite(id, _, _, _, _, _) => single(cmd("sprite", id, xf))
260
+ | Sprite(id: Int, xf: Xf, w: Float, h: Float, image: Str, spin: Float)
282
- Label(id, _, _) => single(cmd("label", id, xf))
261
+ | Label(id: Int, xf: Xf, text: Str)
283
- Button(id, _, _, _, _, pressed) => single(cmd(if pressed then "button-down" else "button", id, xf))
284
- Group(_, _, kids) => foldr(kids, Nil, |k, acc| append(draw(k, xf), acc))
285
-
286
- fun inBox(p: Pt, w: Float, h: Float) -> Bool =
262
+ | Button(id: Int, xf: Xf, w: Float, h: Float, text: Str, pressed: Bool)
287
- p.x >= 0.0 && p.x <= w && p.y >= 0.0 && p.y <= h
263
+ | Group(id: Int, xf: Xf, children: Link[Node])
288
264
 
265
+ fun id(self) -> Int = match self
266
+ Sprite(id, _, _, _, _, _) => id
267
+ Label(id, _, _) => id
268
+ Button(id, _, _, _, _, _) => id
269
+ Group(id, _, _) => id
270
+
271
+ fun xf(self) -> Xf = match self
272
+ Sprite(_, xf, _, _, _, _) => xf
273
+ Label(_, xf, _) => xf
274
+ Button(_, xf, _, _, _, _) => xf
275
+ Group(_, xf, _) => xf
276
+ # The number of nodes in the tree.
277
+ fun count(self) -> Int = match self
278
+ Group(_, _, kids) => 1 + kids.foldl(0, |a, k| a + k.count())
279
+ _ => 1
280
+ # A generic bottom-up rewrite: rebuild the children first, then apply `f`.
281
+ fun mapNodes(self, f: fn(Node) -> Node) -> Node = match self
282
+ Group(id, xf, kids) => f(Group(id: id, xf: xf, children: kids.map(|k| k.mapNodes(f))))
283
+ _ => f(self)
284
+ # Replaces the node with id `target` by `f(node)`; every other node is kept.
285
+ fun update(self, target: Int, f: fn(Node) -> Node) -> Node =
286
+ self.mapNodes(|m| if m.id() == target then f(m) else m)
287
+ # One step of behaviour: a sprite moves by its `spin` per second to the right.
288
+ # Returns a new tree; the old one is untouched.
289
+ fun act(self, dt: Float) -> Node = match self
290
+ Sprite(id, xf, w, h, image, spin) => Sprite(id: id, xf: Xf(x: xf.x + spin * dt, y: xf.y, s: xf.s), w: w, h: h, image: image, spin: spin)
291
+ Group(gid, gxf, kids) => Group(id: gid, xf: gxf, children: kids.map(|k| k.act(dt)))
292
+ _ => self
293
+ # Depth first, parents before children, so later commands draw on top.
294
+ fun draw(self, parent: Xf) -> Link[DrawCmd] =
295
+ xf := thenXf(parent, self.xf())
296
+ match self
297
+ Sprite(id, _, _, _, _, _) => single(cmd("sprite", id, xf))
298
+ Label(id, _, _) => single(cmd("label", id, xf))
299
+ Button(id, _, _, _, _, pressed) => single(cmd(if pressed then "button-down" else "button", id, xf))
300
+ Group(_, _, kids) => kids.foldr(Nil, |k, acc| k.draw(xf).append(acc))
289
- # The id of the topmost node under stage point (px, py), or null. Children drawn
301
+ # The id of the topmost node under stage point (px, py), or null. Children drawn
290
- # later are on top, so they are tried first (in reverse order).
302
+ # later are on top, so they are tried first (in reverse order).
291
- fun findHit(n: Node, parent: Xf, px: Float, py: Float) -> Int? =
303
+ fun findHit(self, parent: Xf, px: Float, py: Float) -> Int? =
292
- xf := thenXf(parent, nodeXf(n))
304
+ xf := thenXf(parent, self.xf())
293
- p := undo(xf, px, py)
305
+ p := undo(xf, px, py)
294
- match n
306
+ match self
295
- Sprite(_, _, w, h, _, _) => hitBox(n, p, w, h)
307
+ Sprite(_, _, w, h, _, _) => hitBox(self, p, w, h)
296
- Button(_, _, bw, bh, _, _) => hitBox(n, p, bw, bh)
308
+ Button(_, _, bw, bh, _, _) => hitBox(self, p, bw, bh)
297
- Label(_, _, _) => null
309
+ Label(_, _, _) => null
298
- Group(_, _, kids) => firstHit(reverse(kids), xf, px, py)
310
+ Group(_, _, kids) => firstHit(kids.reverse(), xf, px, py)
311
+
312
+ fun press(self) -> Node = match self
313
+ Button(id, xf, w, h, text, _) => Button(id: id, xf: xf, w: w, h: h, text: text, pressed: true)
314
+ _ => self
299
315
 
300
316
  fun hitBox(n: Node, p: Pt, w: Float, h: Float) -> Int? =
301
317
  if inBox(p, w, h)
302
- return nodeId(n)
318
+ return n.id()
303
319
  null
304
320
 
305
321
  fun firstHit(kids: Link[Node], xf: Xf, px: Float, py: Float) -> Int? = match kids
306
322
  Nil => null
307
323
  Cons(k, rest) =>
308
- if hit := findHit(k, xf, px, py)
324
+ if hit := k.findHit(xf, px, py)
309
325
  hit
310
326
  else
311
327
  firstHit(rest, xf, px, py)
@@ -318,13 +334,9 @@ record UiEvent(id: Int, kind: UiKind)
318
334
 
319
335
  record Routed(node: Node, events: Link[UiEvent])
320
336
 
321
- fun press(n: Node) -> Node = match n
322
- Button(id, xf, w, h, text, _) => Button(id: id, xf: xf, w: w, h: h, text: text, pressed: true)
323
- _ => n
324
-
325
337
  fun click(root: Node, px: Float, py: Float) -> Routed =
326
- if id := findHit(root, identityXf(), px, py)
338
+ if id := root.findHit(identityXf(), px, py)
327
- Routed(node: update(root, id, press), events: single(UiEvent(id: id, kind: Clicked)))
339
+ Routed(node: root.update(id, |n| n.press()), events: single(UiEvent(id: id, kind: Clicked)))
328
340
  else
329
341
  Routed(node: root, events: Nil)
330
342
 
@@ -337,28 +349,72 @@ fun demoScene() -> Node =
337
349
  menu := Group(id: 4, xf: Xf(x: 0.0, y: 100.0, s: 0.5), children: single(play))
338
350
  Group(id: 1, xf: Xf(x: 100.0, y: 50.0, s: 1.0), children: cons(ship, cons(title, single(menu))))
339
351
 
352
+ # ---------------------------------------------------------------------------
353
+ # 6. Loops, functionally
354
+ # ---------------------------------------------------------------------------
355
+
356
+ # One job (the sum of the even numbers), written three ways. They all agree.
357
+ #
358
+ # 1. Methods: `filter` then `foldl`, each a recursive method on the list.
359
+ fun sumEvensMethods(xs: Link[Int]) -> Int =
360
+ xs.filter(|x| x % 2 == 0).foldl(0, |a, x| a + x)
361
+
362
+ # 2. A `for` over the list's `items()` form, with an `if` clause as the filter.
363
+ fun sumEvensFor(xs: Link[Int]) -> Int =
364
+ total := 0
365
+ for x := range xs.items(), if x % 2 == 0
366
+ total = total + x
367
+ total
368
+
369
+ # 3. A `while` loop with a local cursor, taking the list apart with a pattern.
370
+ fun sumEvensWhile(xs: Link[Int]) -> Int =
371
+ total := 0
372
+ cur := xs
373
+ while true
374
+ if Cons(h, t) := cur
375
+ if h % 2 == 0
376
+ total = total + h
377
+ cur = t
378
+ else
379
+ break
380
+ total
381
+
382
+ # A `for` is an expression when its source has a result: this form rebuilds a
383
+ # list from the body's values, like `map`. (Written for Int lists: a generic
384
+ # `Link[T]` result type is not substituted inside a form body yet.)
385
+ fun* collect(xs: Link[Int]) *Int -> Link[Int] =
386
+ out: Link[Int] := Nil
387
+ for x := range xs.items()
388
+ v := yield x
389
+ out = cons(v, out)
390
+ out.reverse()
391
+
392
+ # The x of the ship (id 2) in a tree's draw commands.
393
+ fun shipX(root: Node) -> Float =
394
+ root.draw(identityXf()).foldl(0.0, |a, c| if c.id == 2 then c.x else a)
395
+
340
396
  # ---------------------------------------------------------------------------
341
397
  # Tests
342
398
  # ---------------------------------------------------------------------------
343
399
 
344
400
  test "link basics"
345
401
  xs := fromTo(1, 6)
346
- assert length(xs) == 5
402
+ assert xs.length() == 5
347
403
  assert joinInts(xs, ",") == "1,2,3,4,5"
348
404
  empty: Link[Int] := Nil
349
- assert isEmpty(empty)
405
+ assert empty.isEmpty()
350
- assert !isEmpty(xs)
406
+ assert !xs.isEmpty()
351
407
  assert sum(xs) == 15
352
408
 
353
409
  test "link combinators"
354
410
  xs := fromTo(1, 6)
355
- assert joinInts(map(xs, |x| x * x), ",") == "1,4,9,16,25"
411
+ assert joinInts(xs.map(|x| x * x), ",") == "1,4,9,16,25"
356
- assert joinInts(filter(xs, |x| x % 2 == 0), ",") == "2,4"
412
+ assert joinInts(xs.filter(|x| x % 2 == 0), ",") == "2,4"
357
- assert joinInts(reverse(xs), ",") == "5,4,3,2,1"
413
+ assert joinInts(xs.reverse(), ",") == "5,4,3,2,1"
358
- assert joinInts(append(take(xs, 2), drop(xs, 3)), ",") == "1,2,4,5"
414
+ assert joinInts(xs.take(2).append(xs.drop(3)), ",") == "1,2,4,5"
359
- assert any(xs, |x| x > 4)
415
+ assert xs.any(|x| x > 4)
360
- assert !all(xs, |x| x > 4)
416
+ assert !xs.all(|x| x > 4)
361
- assert foldr(xs, 0, |x, a| x - a) == 3
417
+ assert xs.foldr(0, |x, a| x - a) == 3
362
418
 
363
419
  test "link sharing"
364
420
  tail := fromTo(2, 4)
@@ -369,11 +425,11 @@ test "link sharing"
369
425
  assert joinInts(tail, " ") == "2 3"
370
426
 
371
427
  test "link first and zip"
372
- assert {first(fromTo(7, 9)) ?: 0} == 7
428
+ assert {fromTo(7, 9).first() ?: 0} == 7
373
- assert {first(fromTo(7, 7)) ?: 0} == 0
429
+ assert {fromTo(7, 7).first() ?: 0} == 0
374
- ps := zip(fromTo(1, 4), fromTo(10, 20))
430
+ ps := fromTo(1, 4).zip(fromTo(10, 20))
375
- assert length(ps) == 3
431
+ assert ps.length() == 3
376
- assert foldl(ps, 0, |a, p| a + p.fst * p.snd) == 1 * 10 + 2 * 11 + 3 * 12
432
+ assert ps.foldl(0, |a, p| a + p.fst * p.snd) == 1 * 10 + 2 * 11 + 3 * 12
377
433
 
378
434
  test "functions as values"
379
435
  inc := adder(1)
@@ -382,70 +438,87 @@ test "functions as values"
382
438
  assert incThenDouble.run(4) == 10
383
439
  assert compose(inc, double).run(4) == 9
384
440
  assert iterate(double.run, 10, 1) == 1024
385
- assert joinInts(map(fromTo(0, 3), adder(100).run), ",") == "100,101,102"
441
+ assert joinInts(fromTo(0, 3).map(adder(100).run), ",") == "100,101,102"
386
442
 
387
443
  test "persistent tree"
388
444
  t := fromLink(cons(5, cons(3, cons(8, cons(1, cons(4, cons(9, Nil)))))))
389
- assert size(t) == 6
445
+ assert t.size() == 6
390
- assert depth(t) == 3
446
+ assert t.depth() == 3
391
- assert member(t, 4)
447
+ assert t.member(4)
392
- assert !member(t, 7)
448
+ assert !t.member(7)
393
- assert joinInts(inOrder(t), " ") == "1 3 4 5 8 9"
449
+ assert joinInts(t.inOrder(), " ") == "1 3 4 5 8 9"
394
- t2 := insert(t, 7)
450
+ t2 := t.insert(7)
395
- assert member(t2, 7)
451
+ assert t2.member(7)
396
- assert !member(t, 7)
452
+ assert !t.member(7)
397
- assert size(t) == 6
453
+ assert t.size() == 6
398
- assert size(t2) == 7
454
+ assert t2.size() == 7
399
- assert {smallest(t2) ?: -1} == 1
455
+ assert {t2.smallest() ?: -1} == 1
400
- assert {smallest(Leaf) ?: -1} == -1
456
+ assert {Leaf.smallest() ?: -1} == -1
401
457
 
402
458
  test "pattern matching"
403
- assert describe(Circle(r: 0.0)) == "point"
459
+ assert Circle(r: 0.0).describe() == "point"
404
- assert describe(Circle(r: 2.0)) == "circle"
460
+ assert Circle(r: 2.0).describe() == "circle"
405
- assert describe(Rect(w: 3.0, h: 3.0)) == "square"
461
+ assert Rect(w: 3.0, h: 3.0).describe() == "square"
406
- assert describe(Rect(w: 4.0, h: 3.0)) == "rect"
462
+ assert Rect(w: 4.0, h: 3.0).describe() == "rect"
407
- assert wide(Rect(w: 4.0, h: 3.0))
463
+ assert Rect(w: 4.0, h: 3.0).wide()
408
- assert !wide(Circle(r: 1.0))
464
+ assert !Circle(r: 1.0).wide()
409
465
  assert firstTwo(fromTo(1, 5)) == "1,2"
410
466
  assert firstTwo(single(7)) == "7"
411
467
  assert firstTwo(Nil) == "empty"
412
468
 
413
469
  test "scene count and draw"
414
470
  scene := demoScene()
415
- assert count(scene) == 5
471
+ assert scene.count() == 5
416
- cmds := draw(scene, identityXf())
472
+ cmds := scene.draw(identityXf())
417
- assert length(cmds) == 3
473
+ assert cmds.length() == 3
418
- kinds := foldr(cmds, "", |c, acc| c.kind + " " + acc)
474
+ kinds := cmds.foldr("", |c, acc| c.kind + " " + acc)
419
475
  assert kinds == "sprite label button "
420
- btn := foldl(cmds, cmd("none", 0, identityXf()), |a, c| if c.id == 5 then c else a)
476
+ btn := cmds.foldl(cmd("none", 0, identityXf()), |a, c| if c.id == 5 then c else a)
421
477
  assert btn.x == 110.0
422
478
  assert btn.y == 160.0
423
479
  assert btn.s == 0.5
424
480
 
425
481
  test "scene act is pure"
426
482
  before := demoScene()
427
- after := act(before, 0.5)
483
+ after := before.act(0.5)
428
- shipX := |root|
429
- foldl(draw(root, identityXf()), 0.0, |a, c| if c.id == 2 then c.x else a)
430
484
  assert shipX(before) == 100.0
431
485
  assert shipX(after) == 105.0
432
486
 
433
487
  test "scene hit test"
434
488
  scene := demoScene()
435
- assert {findHit(scene, identityXf(), 105.0, 55.0) ?: 0} == 2
489
+ assert {scene.findHit(identityXf(), 105.0, 55.0) ?: 0} == 2
436
- assert {findHit(scene, identityXf(), 115.0, 165.0) ?: 0} == 5
490
+ assert {scene.findHit(identityXf(), 115.0, 165.0) ?: 0} == 5
437
- assert {findHit(scene, identityXf(), 0.0, 0.0) ?: 0} == 0
491
+ assert {scene.findHit(identityXf(), 0.0, 0.0) ?: 0} == 0
438
492
 
439
493
  test "scene click routes an event"
440
494
  scene := demoScene()
441
495
  r := click(scene, 115.0, 165.0)
442
- assert length(r.events) == 1
496
+ assert r.events.length() == 1
443
497
  if Cons(e, _) := r.events
444
498
  assert e.id == 5
445
499
  assert e.kind == Clicked
446
- kinds := foldr(draw(r.node, identityXf()), "", |c, acc| c.kind + " " + acc)
500
+ kinds := r.node.draw(identityXf()).foldr("", |c, acc| c.kind + " " + acc)
447
501
  assert kinds == "sprite label button-down "
448
- old := foldr(draw(scene, identityXf()), "", |c, acc| c.kind + " " + acc)
502
+ old := scene.draw(identityXf()).foldr("", |c, acc| c.kind + " " + acc)
449
503
  assert old == "sprite label button "
450
504
  miss := click(scene, 0.0, 0.0)
451
- assert isEmpty(miss.events)
505
+ assert miss.events.isEmpty()
506
+
507
+ test "loops three ways"
508
+ xs := fromTo(1, 11)
509
+ assert sumEvensMethods(xs) == 30
510
+ assert sumEvensFor(xs) == 30
511
+ assert sumEvensWhile(xs) == 30
512
+ empty: Link[Int] := Nil
513
+ assert sumEvensFor(empty) == 0
514
+ assert sumEvensWhile(empty) == 0
515
+
516
+ test "loops build lists too"
517
+ xs := fromTo(1, 5)
518
+ seen := List()
519
+ xs.each(|x| seen.push(x * 2))
520
+ assert seen.length() == 4
521
+ doubled := for x := range collect(xs), if x > 1
522
+ x * 2
523
+ assert joinInts(doubled, ",") == joinInts(xs.filter(|x| x > 1).map(|x| x * 2), ",")
524
+ assert joinInts(doubled, ",") == "4,6,8"