plum

#treesitter#compiler#wasm

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"