plum

#treesitter#compiler#wasm

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"