plum

#treesitter#compiler#wasm

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

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


3e580dbPeter John 2026-09-04T15:54:25+05:30
feat(examples): convert dop_visitor to enum syntax, add OOP trait counterpart
Files changed (2) hide show
  1. examples/dop_visitor.plum +47 -55
  2. examples/oop_visitor.plum +130 -0
examples/dop_visitor.plum CHANGED
@@ -7,67 +7,59 @@
7
7
  # article's Java (sealed interfaces implemented directly by records, switch
8
8
  # with `when` guards and deconstruction):
9
9
  #
10
- # - A bare enum variant naming an already-declared `type` (no `[...]`
10
+ # - Each variant carries its fields inline, right in the enum (e.g.
11
- # payload, e.g. `| FantasyBook`) is sugar for wrapping that whole class as
11
+ # `FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)`), so
12
- # the variant's one payload field, under the SAME name no separate
12
+ # there's no separate `type` declaration to wrapthe named fields are
13
- # wrapper tag (`Fantasy[FantasyBook]`) is needed, and constructing one
14
- # (`FantasyBook(title: ..., ...)`) builds `Book` directly.
13
+ # just call-site sugar (`FantasyBook(title: ..., ...)` or positional
14
+ # `FantasyBook(...)`), and a match arm still destructures purely by
15
+ # position (`FantasyBook(title, pages, hasMythicalCreatures)`).
15
- # - A `when <expr>` guard on a match case (`FantasyBook(b) when
16
+ # - A `when <expr>` guard on a match case (`FantasyBook(title, _,
16
- # b.hasMythicalCreatures => ...`) stands in for Java's guarded switch
17
+ # hasMythicalCreatures) when hasMythicalCreatures => ...`) stands in for
17
- # patterns; a guard that evaluates false falls through to the next case,
18
+ # Java's guarded switch patterns; a guard that evaluates false falls
18
- # same as a genuine pattern mismatch.
19
+ # through to the next case, same as a genuine pattern mismatch.
19
20
  #
20
21
  # The checker's exhaustiveness check on enums stands in for the compile-time
21
22
  # guarantee sealed interfaces + switch give in Java.
22
-
23
+ #
24
+ # DOP vs. OOP (see the `trait`/`type`-per-shape rewrite in
25
+ # `oop_visitor.plum`):
26
+ # - Adding a new operation (a new function that matches on `Book`) touches
27
+ # one place here, and the exhaustiveness checker forces every variant to
28
+ # be handled. In the OOP version it means adding a new method to every
29
+ # `type`, with nothing catching a variant you forgot to update.
30
+ # - Adding a new variant instead means touching every `match` on `Book`
31
+ # here (again caught by exhaustiveness if missed); the OOP version adds
32
+ # it in one new, self-contained `type`, untouched elsewhere.
33
+ # - So DOP fits best when operations change often and the shapes are
34
+ # stable; OOP fits best when shapes multiply and operations are stable.
35
+ # plum has no dynamic dispatch through a trait type (see
36
+ # `oop_visitor.plum`'s header), which tilts things further toward DOP
37
+ # for anything that needs to treat the shapes polymorphically.
38
+
23
- type Reviewer =
39
+ import std/os
24
- name: Str
25
40
 
26
41
  enum Rating =
27
- | Good[Reviewer]
42
+ | Good(name: Str)
28
- | Bad[Reviewer]
43
+ | Bad(name: Str)
29
-
30
- type FantasyBook =
31
- title: Str
32
- pages: Int
33
- hasMythicalCreatures: Bool
34
-
35
- type ScifiBook =
36
- title: Str
37
- pages: Int
38
- theme: Str
39
-
40
- type ChildrensTaleBook =
41
- title: Str
42
- pages: Int
43
- moralLesson: Str
44
-
45
- type NonFictionBook =
46
- title: Str
47
- pages: Int
48
- rating1: Rating
49
- rating2: Rating
50
44
 
51
45
  enum Book =
52
- | FantasyBook
46
+ | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
53
- | ScifiBook
47
+ | ScifiBook(title: Str, pages: Int, theme: Str)
54
- | ChildrensTaleBook
48
+ | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)
55
- | NonFictionBook
49
+ | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)
56
50
 
57
51
  fun collectInterestingInfo(book: Book) -> Str =
58
52
  match book
59
- FantasyBook(b) when b.hasMythicalCreatures => "Fantasy book \"{b.title}\" features mythical creatures"
53
+ FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
60
- FantasyBook(b) => "Fantasy book \"{b.title}\" has no mythical creatures"
54
+ FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
61
- ScifiBook(b) when b.theme == "space exploration" => "Scifi book \"{b.title}\" explores space exploration"
55
+ ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration"
62
- ScifiBook(b) => "Scifi book \"{b.title}\" has theme {b.theme}"
56
+ ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}"
63
- ChildrensTaleBook(b) when b.pages == 0 => "Childrens book \"{b.title}\" is empty"
57
+ ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty"
64
- ChildrensTaleBook(b) => "Childrens book \"{b.title}\" teaches: {b.moralLesson}"
58
+ ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}"
65
- NonFictionBook(b) =>
59
+ NonFictionBook(title, _, rating1, rating2) =>
66
- match b.rating1, b.rating2
60
+ match rating1, rating2
67
- Good(r1), Good(r2) => "Non-fiction book \"{b.title}\" praised by {r1.name} and {r2.name}"
61
+ Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}"
68
- _, _ => "Non-fiction book \"{b.title}\" has mixed reviews"
62
+ _, _ => "Non-fiction book \"{title}\" has mixed reviews"
69
-
70
- extern fun printLn(s: Str)
71
63
 
72
64
  fun describeFantasy() -> Str =
73
65
  collectInterestingInfo(FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: True))
@@ -102,8 +94,8 @@ test "non fiction book praised by both reviewers via nested deconstruction"
102
94
  book := NonFictionBook(
103
95
  title: "True Facts",
104
96
  pages: 120,
105
- rating1: Good(Reviewer(name: "Alice")),
97
+ rating1: Good(name: "Alice"),
106
- rating2: Good(Reviewer(name: "Bob")),
98
+ rating2: Good(name: "Bob"),
107
99
  )
108
100
  assert collectInterestingInfo(book) == "Non-fiction book \"True Facts\" praised by Alice and Bob"
109
101
 
@@ -111,7 +103,7 @@ test "non fiction book with a bad rating reports mixed reviews"
111
103
  book := NonFictionBook(
112
104
  title: "Shaky Claims",
113
105
  pages: 80,
114
- rating1: Good(Reviewer(name: "Alice")),
106
+ rating1: Good(name: "Alice"),
115
- rating2: Bad(Reviewer(name: "Carol")),
107
+ rating2: Bad(name: "Carol"),
116
108
  )
117
109
  assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews"
examples/oop_visitor.plum ADDED
@@ -0,0 +1,130 @@
1
+ # Classic OOP alternative to the DOP version in `dop_visitor.plum`: instead of
2
+ # one sealed `enum` + one exhaustive `match`, each book shape gets its own
3
+ # `type`, declares it implements the `Book` trait, and defines its own
4
+ # `collectInterestingInfo` method — the direct translation of the blog post's
5
+ # Java (sealed interface implemented by records, each overriding a method).
6
+ # Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
7
+ #
8
+ # One real limitation this example runs into: plum's `trait` is a
9
+ # compile-time structural check only ("does this type declare these
10
+ # methods with matching signatures?") — there is no vtable or runtime
11
+ # dispatch. A function can't take a bare `book: Book` parameter and call
12
+ # `book.collectInterestingInfo()` polymorphically across different concrete
13
+ # types, and there's no way to hold a heterogeneous list of `Book`s. So
14
+ # unlike the Java original (or the enum/match version), there's no single
15
+ # `describe(book: Book)` entry point here — each concrete type's method is
16
+ # called directly at its own call site.
17
+ #
18
+ # OOP vs. DOP (see the `enum`/`match` version in `dop_visitor.plum`):
19
+ # - Adding a new variant (a new `type(Book) = ...`) is self-contained here
20
+ # and doesn't touch existing types. In the DOP version it means adding
21
+ # a new arm to every `match` on `Book` (caught by the checker's
22
+ # exhaustiveness check if missed).
23
+ # - Adding a new operation instead means adding a method to every `type`
24
+ # here, with nothing enforcing that you updated all of them; the DOP
25
+ # version adds one new function with one exhaustive `match`.
26
+ # - So OOP fits best when shapes multiply and operations are stable; DOP
27
+ # fits best when operations change often and the shapes are stable.
28
+ # plum's lack of dynamic dispatch through a trait type (see above)
29
+ # removes OOP's usual "call through one interface" benefit entirely,
30
+ # which is a real cost specific to this language.
31
+
32
+ import std/os
33
+
34
+ enum Rating =
35
+ | Good(name: Str)
36
+ | Bad(name: Str)
37
+
38
+ trait Book =
39
+ collectInterestingInfo() -> Str
40
+
41
+ type FantasyBook(Book) =
42
+ title: Str
43
+ pages: Int
44
+ hasMythicalCreatures: Bool
45
+
46
+ fun collectInterestingInfo(self) -> Str =
47
+ if self.hasMythicalCreatures
48
+ "Fantasy book \"{self.title}\" features mythical creatures"
49
+ else
50
+ "Fantasy book \"{self.title}\" has no mythical creatures"
51
+
52
+ type ScifiBook(Book) =
53
+ title: Str
54
+ pages: Int
55
+ theme: Str
56
+
57
+ fun collectInterestingInfo(self) -> Str =
58
+ if self.theme == "space exploration"
59
+ "Scifi book \"{self.title}\" explores space exploration"
60
+ else
61
+ "Scifi book \"{self.title}\" has theme {self.theme}"
62
+
63
+ type ChildrensTaleBook(Book) =
64
+ title: Str
65
+ pages: Int
66
+ moralLesson: Str
67
+
68
+ fun collectInterestingInfo(self) -> Str =
69
+ if self.pages == 0
70
+ "Childrens book \"{self.title}\" is empty"
71
+ else
72
+ "Childrens book \"{self.title}\" teaches: {self.moralLesson}"
73
+
74
+ type NonFictionBook(Book) =
75
+ title: Str
76
+ pages: Int
77
+ rating1: Rating
78
+ rating2: Rating
79
+
80
+ fun collectInterestingInfo(self) -> Str =
81
+ match self.rating1, self.rating2
82
+ Good(name1), Good(name2) => "Non-fiction book \"{self.title}\" praised by {name1} and {name2}"
83
+ _, _ => "Non-fiction book \"{self.title}\" has mixed reviews"
84
+
85
+ fun describeFantasy() -> Str =
86
+ FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: True).collectInterestingInfo()
87
+
88
+ fun main() =
89
+ printLn(describeFantasy())
90
+
91
+ test "fantasy book with mythical creatures is called out"
92
+ assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures"
93
+
94
+ test "fantasy book without mythical creatures is called out"
95
+ book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: False)
96
+ assert book.collectInterestingInfo() == "Fantasy book \"Plain Fantasy\" has no mythical creatures"
97
+
98
+ test "scifi book with space exploration theme is called out"
99
+ book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration")
100
+ assert book.collectInterestingInfo() == "Scifi book \"Starbound\" explores space exploration"
101
+
102
+ test "scifi book with other theme falls through to generic case"
103
+ book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel")
104
+ assert book.collectInterestingInfo() == "Scifi book \"Timeloop\" has theme time travel"
105
+
106
+ test "empty childrens book is handled via a plain if on pages"
107
+ book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
108
+ assert book.collectInterestingInfo() == "Childrens book \"Nothing\" is empty"
109
+
110
+ test "childrens book with pages reports its moral lesson"
111
+ book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins")
112
+ assert book.collectInterestingInfo() == "Childrens book \"The Tortoise\" teaches: patience wins"
113
+
114
+ test "non fiction book praised by both reviewers via nested match"
115
+ book := NonFictionBook(
116
+ title: "True Facts",
117
+ pages: 120,
118
+ rating1: Good(name: "Alice"),
119
+ rating2: Good(name: "Bob"),
120
+ )
121
+ assert book.collectInterestingInfo() == "Non-fiction book \"True Facts\" praised by Alice and Bob"
122
+
123
+ test "non fiction book with a bad rating reports mixed reviews"
124
+ book := NonFictionBook(
125
+ title: "Shaky Claims",
126
+ pages: 80,
127
+ rating1: Good(name: "Alice"),
128
+ rating2: Bad(name: "Carol"),
129
+ )
130
+ assert book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews"