plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a1704af
— Peter John
2026-11-02T19:50:25+05:30
T8: docs and examples
- README.md +37 -0
- plum-examples/trait_scene.plum +50 -0
- plum-examples/traits.plum +80 -0
README.md
CHANGED
|
@@ -528,6 +528,43 @@ enum Step(n: Int) =
|
|
|
528
528
|
| ReadMax(20)
|
|
529
529
|
```
|
|
530
530
|
|
|
531
|
+
### Traits
|
|
532
|
+
|
|
533
|
+
A `trait` bundles fields and methods. A method without a body is required; one with a body is a default that a record may replace. A `record` embeds traits by listing them first in its field list, gains their fields (set in its constructor like its own) and default methods, and converts to each trait wherever one is expected:
|
|
534
|
+
|
|
535
|
+
```plum
|
|
536
|
+
trait Entity(pos: Vec2, z: Int) =
|
|
537
|
+
fun update(self, dt: Float) # required: every embedding record defines it
|
|
538
|
+
fun draw(self)
|
|
539
|
+
fun moveBy(self, d: Vec2) = # default
|
|
540
|
+
self.pos = self.pos + d
|
|
541
|
+
fun drawTwice(self) =
|
|
542
|
+
self.draw() # runs the record's own draw
|
|
543
|
+
self.draw()
|
|
544
|
+
|
|
545
|
+
record House(Entity, Clickable, kind: Int) =
|
|
546
|
+
fun update(self, dt: Float) = ...
|
|
547
|
+
fun draw(self) = ...
|
|
548
|
+
|
|
549
|
+
es := List[Entity]()
|
|
550
|
+
es.push(House(pos: p, z: 0, hits: 0, kind: 1)) # House converts to Entity
|
|
551
|
+
for e := range es
|
|
552
|
+
e.update(dt) # runs House.update
|
|
553
|
+
e.pos = e.pos + drift # fields read and write through the trait
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Writes through a trait are seen by the record value (`e.pos = p` changes the `House`); a nested write such as `e.pos.x = 1` is rejected, assign the whole field. `match` narrows a trait value back to a record and always needs a `_` case; a record pattern binds all of the record's fields, trait fields first:
|
|
557
|
+
|
|
558
|
+
```plum
|
|
559
|
+
match e
|
|
560
|
+
House(_, _, _, kind) => kind
|
|
561
|
+
_ => -1
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Everything is resolved statically from the whole program; there are no vtables. Compile errors: a missing required method, two traits (or a trait and the record) declaring the same field, two traits with the same default method that the record doesn't define itself, embedding something that isn't a trait, a trait in a `struct`, a method call the trait doesn't declare, and a `match` on a trait without `_`.
|
|
565
|
+
|
|
566
|
+
Not in v1: generic traits, traits embedding traits, `fun*` in traits, struct embedding, and `==` on trait values. See `plum-examples/traits.plum` and `plum-examples/trait_scene.plum`.
|
|
567
|
+
|
|
531
568
|
### Methods, `self` and field access
|
|
532
569
|
|
|
533
570
|
A `fun` declared indented directly inside an `enum`, `struct` or `record` body whose first parameter is `self` is a method, called on a value (`c.getAge()`). One without `self` is a static function, called on the type (`Cat.named("Tom")`), and can't use `self`. Calling a static on a value, or a method on the type, is an error:
|
plum-examples/trait_scene.plum
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# A tiny pure scene: a `Scene` holds `List[Entity]`; the engine side knows only
|
|
2
|
+
# the trait, the records fill it in. No game host needed.
|
|
3
|
+
module trait_scene
|
|
4
|
+
import std/Str
|
|
5
|
+
import std/Int
|
|
6
|
+
import std/List
|
|
7
|
+
|
|
8
|
+
trait Entity(x: Int, y: Int) =
|
|
9
|
+
fun update(self, dt: Int)
|
|
10
|
+
fun draw(self) -> Int
|
|
11
|
+
fun hit(self, px: Int, py: Int) -> Bool = px == self.x && py == self.y
|
|
12
|
+
|
|
13
|
+
record House(Entity, floors: Int) =
|
|
14
|
+
fun update(self, dt: Int) =
|
|
15
|
+
return
|
|
16
|
+
fun draw(self) -> Int = self.floors * 10
|
|
17
|
+
|
|
18
|
+
record Walker(Entity, speed: Int) =
|
|
19
|
+
fun update(self, dt: Int) =
|
|
20
|
+
self.x = self.x + self.speed * dt
|
|
21
|
+
fun draw(self) -> Int = self.x
|
|
22
|
+
|
|
23
|
+
record Scene(entities: List[Entity]) =
|
|
24
|
+
fun update(self, dt: Int) =
|
|
25
|
+
for e := range self.entities
|
|
26
|
+
e.update(dt)
|
|
27
|
+
fun drawAll(self) -> Int =
|
|
28
|
+
total := 0
|
|
29
|
+
for e := range self.entities
|
|
30
|
+
total = total + e.draw()
|
|
31
|
+
total
|
|
32
|
+
|
|
33
|
+
fun makeScene() -> Scene =
|
|
34
|
+
es: List[Entity] := List[Entity]()
|
|
35
|
+
es.push(House(x: 0, y: 0, floors: 2))
|
|
36
|
+
es.push(Walker(x: 3, y: 1, speed: 2))
|
|
37
|
+
Scene(entities: es)
|
|
38
|
+
|
|
39
|
+
test "scene_updates_and_draws"
|
|
40
|
+
s := makeScene()
|
|
41
|
+
assert s.drawAll() == 23
|
|
42
|
+
s.update(5)
|
|
43
|
+
assert s.drawAll() == 33
|
|
44
|
+
|
|
45
|
+
test "scene_hit_default"
|
|
46
|
+
s := makeScene()
|
|
47
|
+
assert s.entities.at(1).hit(3, 1)
|
|
48
|
+
s.update(1)
|
|
49
|
+
assert s.entities.at(1).hit(5, 1)
|
|
50
|
+
assert !s.entities.at(0).hit(1, 1)
|
plum-examples/traits.plum
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Traits: `trait Entity(pos: Vec2, z: Int) = ...` bundles fields and methods. A
|
|
2
|
+
# `record` embeds traits by listing them first in its field list, gains their
|
|
3
|
+
# fields and default methods, and converts to each trait where one is expected.
|
|
4
|
+
# A method without a body is required; one with a body is a default.
|
|
5
|
+
module traits
|
|
6
|
+
import std/Str
|
|
7
|
+
import std/Int
|
|
8
|
+
import std/Float
|
|
9
|
+
import std/List
|
|
10
|
+
|
|
11
|
+
struct Vec2(x: Int, y: Int) =
|
|
12
|
+
fun add(self, o: Vec2) -> Vec2 = Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
13
|
+
|
|
14
|
+
trait Entity(pos: Vec2, z: Int) =
|
|
15
|
+
fun update(self, dt: Int) -> Int
|
|
16
|
+
fun draw(self) -> Int
|
|
17
|
+
fun moveBy(self, d: Vec2) =
|
|
18
|
+
self.pos = self.pos + d
|
|
19
|
+
fun drawTwice(self) -> Int = self.draw() * 2
|
|
20
|
+
|
|
21
|
+
trait Clickable(hits: Int) =
|
|
22
|
+
fun onClick(self) -> Int
|
|
23
|
+
|
|
24
|
+
record House(Entity, Clickable, kind: Int) =
|
|
25
|
+
fun update(self, dt: Int) -> Int = self.kind * dt
|
|
26
|
+
fun draw(self) -> Int = 100 + self.pos.x
|
|
27
|
+
fun onClick(self) -> Int = 7
|
|
28
|
+
|
|
29
|
+
record Tree(Entity, age: Int) =
|
|
30
|
+
fun update(self, dt: Int) -> Int = self.age + dt
|
|
31
|
+
fun draw(self) -> Int = 200 + self.pos.x
|
|
32
|
+
fun drawTwice(self) -> Int = 1
|
|
33
|
+
|
|
34
|
+
fun house() -> House = House(pos: Vec2(x: 1, y: 2), z: 0, hits: 0, kind: 3)
|
|
35
|
+
|
|
36
|
+
fun tree() -> Tree = Tree(pos: Vec2(x: 5, y: 5), z: 1, age: 10)
|
|
37
|
+
|
|
38
|
+
fun clickIt(c: Clickable) -> Int = c.onClick() + c.hits
|
|
39
|
+
|
|
40
|
+
fun describe(e: Entity) -> Int =
|
|
41
|
+
match e
|
|
42
|
+
House(_, _, _, kind) => kind
|
|
43
|
+
_ => -1
|
|
44
|
+
|
|
45
|
+
test "fields_through_trait"
|
|
46
|
+
h := house()
|
|
47
|
+
e: Entity := h
|
|
48
|
+
assert e.z == 0
|
|
49
|
+
e.pos = Vec2(x: 9, y: 9)
|
|
50
|
+
assert h.pos.x == 9
|
|
51
|
+
e.z = 4
|
|
52
|
+
assert h.z == 4
|
|
53
|
+
|
|
54
|
+
test "list_of_entities_dispatch"
|
|
55
|
+
es: List[Entity] := List[Entity]()
|
|
56
|
+
es.push(house())
|
|
57
|
+
es.push(tree())
|
|
58
|
+
assert es.at(0).update(2) == 6
|
|
59
|
+
assert es.at(1).update(2) == 12
|
|
60
|
+
assert es.at(0).draw() == 101
|
|
61
|
+
assert es.at(1).draw() == 205
|
|
62
|
+
|
|
63
|
+
test "default_changes_pos"
|
|
64
|
+
h := house()
|
|
65
|
+
e: Entity := h
|
|
66
|
+
e.moveBy(Vec2(x: 10, y: 20))
|
|
67
|
+
assert h.pos.x == 11
|
|
68
|
+
assert e.pos.y == 22
|
|
69
|
+
|
|
70
|
+
test "default_calls_override"
|
|
71
|
+
assert house().drawTwice() == 202
|
|
72
|
+
e: Entity := tree()
|
|
73
|
+
assert e.drawTwice() == 1
|
|
74
|
+
|
|
75
|
+
test "match_with_wildcard"
|
|
76
|
+
assert describe(house()) == 3
|
|
77
|
+
assert describe(tree()) == -1
|
|
78
|
+
|
|
79
|
+
test "record_in_two_traits"
|
|
80
|
+
assert clickIt(house()) == 7
|