plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-examples/dop_visitor.plum
| 4a2384c | 1 | # Data-oriented alternative to the Visitor pattern. |
| 4a2384c | 2 | # Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop |
| 4a2384c | 3 | # |
| 4a2384c | 4 | # Instead of a Visitable/BookVisitor double-dispatch hierarchy, the shape of |
| 4a2384c | 5 | # the data is declared once with a sealed `enum` and every operation on it is |
| 4a2384c | 6 | # a single exhaustive `match`. Two features make this read close to the |
| 4a2384c | 7 | # article's Java (sealed interfaces implemented directly by records, switch |
| 4a2384c | 8 | # with `when` guards and deconstruction): |
| 4a2384c | 9 | # |
| 3e580db | 10 | # - Each variant carries its fields inline, right in the enum (e.g. |
| 3e580db | 11 | # `FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)`), so |
| 3e580db | 12 | # there's no separate `type` declaration to wrap — the named fields are |
| 3e580db | 13 | # just call-site sugar (`FantasyBook(title: ..., ...)` or positional |
| 3e580db | 14 | # `FantasyBook(...)`), and a match arm still destructures purely by |
| 3e580db | 15 | # position (`FantasyBook(title, pages, hasMythicalCreatures)`). |
| 3e580db | 16 | # - A `when <expr>` guard on a match case (`FantasyBook(title, _, |
| 3e580db | 17 | # hasMythicalCreatures) when hasMythicalCreatures => ...`) stands in for |
| 3e580db | 18 | # Java's guarded switch patterns; a guard that evaluates false falls |
| 3e580db | 19 | # through to the next case, same as a genuine pattern mismatch. |
| 4a2384c | 20 | # |
| 4a2384c | 21 | # The checker's exhaustiveness check on enums stands in for the compile-time |
| 4a2384c | 22 | # guarantee sealed interfaces + switch give in Java. |
| 3e580db | 23 | # |
| 3e580db | 24 | # DOP vs. OOP (see the `trait`/`type`-per-shape rewrite in |
| 3e580db | 25 | # `oop_visitor.plum`): |
| 3e580db | 26 | # - Adding a new operation (a new function that matches on `Book`) touches |
| 3e580db | 27 | # one place here, and the exhaustiveness checker forces every variant to |
| 3e580db | 28 | # be handled. In the OOP version it means adding a new method to every |
| 3e580db | 29 | # `type`, with nothing catching a variant you forgot to update. |
| 3e580db | 30 | # - Adding a new variant instead means touching every `match` on `Book` |
| 3e580db | 31 | # here (again caught by exhaustiveness if missed); the OOP version adds |
| 3e580db | 32 | # it in one new, self-contained `type`, untouched elsewhere. |
| 3e580db | 33 | # - So DOP fits best when operations change often and the shapes are |
| 3e580db | 34 | # stable; OOP fits best when shapes multiply and operations are stable. |
| 3e580db | 35 | # plum has no dynamic dispatch through a trait type (see |
| 3e580db | 36 | # `oop_visitor.plum`'s header), which tilts things further toward DOP |
| 3e580db | 37 | # for anything that needs to treat the shapes polymorphically. |
| 3e580db | 38 | |
| 29313cb | 39 | import std/Os |
| 73b5e55 | 40 | import std/Bool |
| ca5fd6f | 41 | import std/Number |
| 29313cb | 42 | import std/Str |
| 4a2384c | 43 | |
| 4a2384c | 44 | enum Rating = |
| 3e580db | 45 | | Good(name: Str) |
| 3e580db | 46 | | Bad(name: Str) |
| 4a2384c | 47 | |
| 4a2384c | 48 | enum Book = |
| 3e580db | 49 | | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool) |
| 3e580db | 50 | | ScifiBook(title: Str, pages: Int, theme: Str) |
| 3e580db | 51 | | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str) |
| 3e580db | 52 | | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating) |
| 4a2384c | 53 | |
| 4a2384c | 54 | fun collectInterestingInfo(book: Book) -> Str = |
| 4a2384c | 55 | match book |
| 3e580db | 56 | FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures" |
| 3e580db | 57 | FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures" |
| 3e580db | 58 | ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration" |
| 3e580db | 59 | ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}" |
| 3e580db | 60 | ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty" |
| 3e580db | 61 | ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}" |
| 3e580db | 62 | NonFictionBook(title, _, rating1, rating2) => |
| 3e580db | 63 | match rating1, rating2 |
| 3e580db | 64 | Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}" |
| 3e580db | 65 | _, _ => "Non-fiction book \"{title}\" has mixed reviews" |
| 4a2384c | 66 | |
| 4a2384c | 67 | fun describeFantasy() -> Str = |
| 4a2384c | 68 | collectInterestingInfo(FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: True)) |
| 4a2384c | 69 | |
| 4a2384c | 70 | fun main() = |
| 4a2384c | 71 | printLn(describeFantasy()) |
| 4a2384c | 72 | |
| 4a2384c | 73 | test "fantasy book with mythical creatures is called out" |
| a9a0147 | 74 | assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures" |
| 4a2384c | 75 | |
| 4a2384c | 76 | test "fantasy book without mythical creatures is called out" |
| 4a2384c | 77 | book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: False) |
| a9a0147 | 78 | assert collectInterestingInfo(book) == "Fantasy book \"Plain Fantasy\" has no mythical creatures" |
| 4a2384c | 79 | |
| 4a2384c | 80 | test "scifi book with space exploration theme is called out" |
| 4a2384c | 81 | book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration") |
| a9a0147 | 82 | assert collectInterestingInfo(book) == "Scifi book \"Starbound\" explores space exploration" |
| 4a2384c | 83 | |
| 4a2384c | 84 | test "scifi book with other theme falls through to generic case" |
| 4a2384c | 85 | book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel") |
| a9a0147 | 86 | assert collectInterestingInfo(book) == "Scifi book \"Timeloop\" has theme time travel" |
| 4a2384c | 87 | |
| 4a2384c | 88 | test "empty childrens book is handled via a when guard on pages" |
| 4a2384c | 89 | book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none") |
| a9a0147 | 90 | assert collectInterestingInfo(book) == "Childrens book \"Nothing\" is empty" |
| 4a2384c | 91 | |
| 4a2384c | 92 | test "childrens book with pages reports its moral lesson" |
| 4a2384c | 93 | book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins") |
| a9a0147 | 94 | assert collectInterestingInfo(book) == "Childrens book \"The Tortoise\" teaches: patience wins" |
| 4a2384c | 95 | |
| 4a2384c | 96 | test "non fiction book praised by both reviewers via nested deconstruction" |
| 4a2384c | 97 | book := NonFictionBook( |
| 4a2384c | 98 | title: "True Facts", |
| 4a2384c | 99 | pages: 120, |
| 3e580db | 100 | rating1: Good(name: "Alice"), |
| 3e580db | 101 | rating2: Good(name: "Bob"), |
| 4a2384c | 102 | ) |
| a9a0147 | 103 | assert collectInterestingInfo(book) == "Non-fiction book \"True Facts\" praised by Alice and Bob" |
| 4a2384c | 104 | |
| 4a2384c | 105 | test "non fiction book with a bad rating reports mixed reviews" |
| 4a2384c | 106 | book := NonFictionBook( |
| 4a2384c | 107 | title: "Shaky Claims", |
| 4a2384c | 108 | pages: 80, |
| 3e580db | 109 | rating1: Good(name: "Alice"), |
| 3e580db | 110 | rating2: Bad(name: "Carol"), |
| 4a2384c | 111 | ) |
| a9a0147 | 112 | assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews" |