plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-examples/oop_visitor.plum
| 3e580db | 1 | # Classic OOP alternative to the DOP version in `dop_visitor.plum`: instead of |
| 3e580db | 2 | # one sealed `enum` + one exhaustive `match`, each book shape gets its own |
| 3e580db | 3 | # `type`, declares it implements the `Book` trait, and defines its own |
| 3e580db | 4 | # `collectInterestingInfo` method — the direct translation of the blog post's |
| 3e580db | 5 | # Java (sealed interface implemented by records, each overriding a method). |
| 3e580db | 6 | # Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop |
| 3e580db | 7 | # |
| 3e580db | 8 | # One real limitation this example runs into: plum's `trait` is a |
| 3e580db | 9 | # compile-time structural check only ("does this type declare these |
| 3e580db | 10 | # methods with matching signatures?") — there is no vtable or runtime |
| 3e580db | 11 | # dispatch. A function can't take a bare `book: Book` parameter and call |
| 3e580db | 12 | # `book.collectInterestingInfo()` polymorphically across different concrete |
| 3e580db | 13 | # types, and there's no way to hold a heterogeneous list of `Book`s. So |
| 3e580db | 14 | # unlike the Java original (or the enum/match version), there's no single |
| 3e580db | 15 | # `describe(book: Book)` entry point here — each concrete type's method is |
| 3e580db | 16 | # called directly at its own call site. |
| 3e580db | 17 | # |
| 3e580db | 18 | # OOP vs. DOP (see the `enum`/`match` version in `dop_visitor.plum`): |
| 3e580db | 19 | # - Adding a new variant (a new `type(Book) = ...`) is self-contained here |
| 3e580db | 20 | # and doesn't touch existing types. In the DOP version it means adding |
| 3e580db | 21 | # a new arm to every `match` on `Book` (caught by the checker's |
| 3e580db | 22 | # exhaustiveness check if missed). |
| 3e580db | 23 | # - Adding a new operation instead means adding a method to every `type` |
| 3e580db | 24 | # here, with nothing enforcing that you updated all of them; the DOP |
| 3e580db | 25 | # version adds one new function with one exhaustive `match`. |
| 3e580db | 26 | # - So OOP fits best when shapes multiply and operations are stable; DOP |
| 3e580db | 27 | # fits best when operations change often and the shapes are stable. |
| 3e580db | 28 | # plum's lack of dynamic dispatch through a trait type (see above) |
| 3e580db | 29 | # removes OOP's usual "call through one interface" benefit entirely, |
| 3e580db | 30 | # which is a real cost specific to this language. |
| 3e580db | 31 | |
| 29313cb | 32 | import std/Os |
| 73b5e55 | 33 | import std/Bool |
| ca5fd6f | 34 | import std/Number |
| 29313cb | 35 | import std/Str |
| 3e580db | 36 | |
| 3e580db | 37 | enum Rating = |
| 3e580db | 38 | | Good(name: Str) |
| 3e580db | 39 | | Bad(name: Str) |
| 3e580db | 40 | |
| 3e580db | 41 | trait Book = |
| 3e580db | 42 | collectInterestingInfo() -> Str |
| 3e580db | 43 | |
| 5f2f962 | 44 | enum FantasyBook(Book) = |
| 5f2f962 | 45 | | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool) |
| 3e580db | 46 | |
| 3e580db | 47 | fun collectInterestingInfo(self) -> Str = |
| 3e580db | 48 | if self.hasMythicalCreatures |
| 3e580db | 49 | "Fantasy book \"{self.title}\" features mythical creatures" |
| 3e580db | 50 | else |
| 3e580db | 51 | "Fantasy book \"{self.title}\" has no mythical creatures" |
| 3e580db | 52 | |
| 5f2f962 | 53 | enum ScifiBook(Book) = |
| 5f2f962 | 54 | | ScifiBook(title: Str, pages: Int, theme: Str) |
| 3e580db | 55 | |
| 3e580db | 56 | fun collectInterestingInfo(self) -> Str = |
| 3e580db | 57 | if self.theme == "space exploration" |
| 3e580db | 58 | "Scifi book \"{self.title}\" explores space exploration" |
| 3e580db | 59 | else |
| 3e580db | 60 | "Scifi book \"{self.title}\" has theme {self.theme}" |
| 3e580db | 61 | |
| 5f2f962 | 62 | enum ChildrensTaleBook(Book) = |
| 5f2f962 | 63 | | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str) |
| 3e580db | 64 | |
| 3e580db | 65 | fun collectInterestingInfo(self) -> Str = |
| 3e580db | 66 | if self.pages == 0 |
| 3e580db | 67 | "Childrens book \"{self.title}\" is empty" |
| 3e580db | 68 | else |
| 3e580db | 69 | "Childrens book \"{self.title}\" teaches: {self.moralLesson}" |
| 3e580db | 70 | |
| 5f2f962 | 71 | enum NonFictionBook(Book) = |
| 5f2f962 | 72 | | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating) |
| 3e580db | 73 | |
| 3e580db | 74 | fun collectInterestingInfo(self) -> Str = |
| 3e580db | 75 | match self.rating1, self.rating2 |
| 3e580db | 76 | Good(name1), Good(name2) => "Non-fiction book \"{self.title}\" praised by {name1} and {name2}" |
| 3e580db | 77 | _, _ => "Non-fiction book \"{self.title}\" has mixed reviews" |
| 3e580db | 78 | |
| 3e580db | 79 | fun describeFantasy() -> Str = |
| 3e580db | 80 | FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: True).collectInterestingInfo() |
| 3e580db | 81 | |
| 3e580db | 82 | fun main() = |
| 3e580db | 83 | printLn(describeFantasy()) |
| 3e580db | 84 | |
| 3e580db | 85 | test "fantasy book with mythical creatures is called out" |
| 3e580db | 86 | assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures" |
| 3e580db | 87 | |
| 3e580db | 88 | test "fantasy book without mythical creatures is called out" |
| 3e580db | 89 | book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: False) |
| 3e580db | 90 | assert book.collectInterestingInfo() == "Fantasy book \"Plain Fantasy\" has no mythical creatures" |
| 3e580db | 91 | |
| 3e580db | 92 | test "scifi book with space exploration theme is called out" |
| 3e580db | 93 | book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration") |
| 3e580db | 94 | assert book.collectInterestingInfo() == "Scifi book \"Starbound\" explores space exploration" |
| 3e580db | 95 | |
| 3e580db | 96 | test "scifi book with other theme falls through to generic case" |
| 3e580db | 97 | book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel") |
| 3e580db | 98 | assert book.collectInterestingInfo() == "Scifi book \"Timeloop\" has theme time travel" |
| 3e580db | 99 | |
| 3e580db | 100 | test "empty childrens book is handled via a plain if on pages" |
| 3e580db | 101 | book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none") |
| 3e580db | 102 | assert book.collectInterestingInfo() == "Childrens book \"Nothing\" is empty" |
| 3e580db | 103 | |
| 3e580db | 104 | test "childrens book with pages reports its moral lesson" |
| 3e580db | 105 | book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins") |
| 3e580db | 106 | assert book.collectInterestingInfo() == "Childrens book \"The Tortoise\" teaches: patience wins" |
| 3e580db | 107 | |
| 3e580db | 108 | test "non fiction book praised by both reviewers via nested match" |
| 3e580db | 109 | book := NonFictionBook( |
| 3e580db | 110 | title: "True Facts", |
| 3e580db | 111 | pages: 120, |
| 3e580db | 112 | rating1: Good(name: "Alice"), |
| 3e580db | 113 | rating2: Good(name: "Bob"), |
| 3e580db | 114 | ) |
| 3e580db | 115 | assert book.collectInterestingInfo() == "Non-fiction book \"True Facts\" praised by Alice and Bob" |
| 3e580db | 116 | |
| 3e580db | 117 | test "non fiction book with a bad rating reports mixed reviews" |
| 3e580db | 118 | book := NonFictionBook( |
| 3e580db | 119 | title: "Shaky Claims", |
| 3e580db | 120 | pages: 80, |
| 3e580db | 121 | rating1: Good(name: "Alice"), |
| 3e580db | 122 | rating2: Bad(name: "Carol"), |
| 3e580db | 123 | ) |
| 3e580db | 124 | assert book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews" |