plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
3e580db
— Peter John
2026-09-04T15:54:25+05:30
feat(examples): convert dop_visitor to enum syntax, add OOP trait counterpart
- examples/dop_visitor.plum +47 -55
- 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
|
-
# -
|
|
10
|
+
# - Each variant carries its fields inline, right in the enum (e.g.
|
|
11
|
-
#
|
|
11
|
+
# `FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)`), so
|
|
12
|
-
#
|
|
12
|
+
# there's no separate `type` declaration to wrap — the named fields are
|
|
13
|
-
# wrapper tag (`Fantasy[FantasyBook]`) is needed, and constructing one
|
|
14
|
-
# (`FantasyBook(title: ..., ...)`
|
|
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(
|
|
16
|
+
# - A `when <expr>` guard on a match case (`FantasyBook(title, _,
|
|
16
|
-
#
|
|
17
|
+
# hasMythicalCreatures) when hasMythicalCreatures => ...`) stands in for
|
|
17
|
-
# patterns; a guard that evaluates false falls
|
|
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
|
-
|
|
39
|
+
import std/os
|
|
24
|
-
name: Str
|
|
25
40
|
|
|
26
41
|
enum Rating =
|
|
27
|
-
| Good
|
|
42
|
+
| Good(name: Str)
|
|
28
|
-
| Bad
|
|
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(
|
|
53
|
+
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
|
|
60
|
-
FantasyBook(
|
|
54
|
+
FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
|
|
61
|
-
ScifiBook(
|
|
55
|
+
ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration"
|
|
62
|
-
ScifiBook(
|
|
56
|
+
ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}"
|
|
63
|
-
ChildrensTaleBook(
|
|
57
|
+
ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty"
|
|
64
|
-
ChildrensTaleBook(
|
|
58
|
+
ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}"
|
|
65
|
-
NonFictionBook(
|
|
59
|
+
NonFictionBook(title, _, rating1, rating2) =>
|
|
66
|
-
match
|
|
60
|
+
match rating1, rating2
|
|
67
|
-
Good(
|
|
61
|
+
Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}"
|
|
68
|
-
_, _ => "Non-fiction book \"{
|
|
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(
|
|
97
|
+
rating1: Good(name: "Alice"),
|
|
106
|
-
rating2: Good(
|
|
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(
|
|
106
|
+
rating1: Good(name: "Alice"),
|
|
115
|
-
rating2: Bad(
|
|
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"
|