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
# Data-oriented alternative to the Visitor pattern.
# Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
#
# Instead of a Visitable/BookVisitor double-dispatch hierarchy, the shape of
# the data is declared once with a sealed `enum` and every operation on it is
# a single exhaustive `match`. Two features make this read close to the
# article's Java (sealed interfaces implemented directly by records, switch
# with `when` guards and deconstruction):
#
#   - Each variant carries its fields inline, right in the enum (e.g.
#     `FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)`), so
#     there's no separate `type` declaration to wrap — the named fields are
#     just call-site sugar (`FantasyBook(title: ..., ...)` or positional
#     `FantasyBook(...)`), and a match arm still destructures purely by
#     position (`FantasyBook(title, pages, hasMythicalCreatures)`).
#   - A `when <expr>` guard on a match case (`FantasyBook(title, _,
#     hasMythicalCreatures) when hasMythicalCreatures => ...`) stands in for
#     Java's guarded switch patterns; a guard that evaluates false falls
#     through to the next case, same as a genuine pattern mismatch.
#
# The checker's exhaustiveness check on enums stands in for the compile-time
# guarantee sealed interfaces + switch give in Java.
#
# DOP vs. OOP (see the `trait`/`type`-per-shape rewrite in
# `oop_visitor.plum`):
#   - Adding a new operation (a new function that matches on `Book`) touches
#     one place here, and the exhaustiveness checker forces every variant to
#     be handled. In the OOP version it means adding a new method to every
#     `type`, with nothing catching a variant you forgot to update.
#   - Adding a new variant instead means touching every `match` on `Book`
#     here (again caught by exhaustiveness if missed); the OOP version adds
#     it in one new, self-contained `type`, untouched elsewhere.
#   - So DOP fits best when operations change often and the shapes are
#     stable; OOP fits best when shapes multiply and operations are stable.
#     plum has no dynamic dispatch through a trait type (see
#     `oop_visitor.plum`'s header), which tilts things further toward DOP
#     for anything that needs to treat the shapes polymorphically.

import std/Os
import std/Bool
import std/Number
import std/Str

enum Rating =
  | Good(name: Str)
  | Bad(name: Str)

enum Book =
  | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
  | ScifiBook(title: Str, pages: Int, theme: Str)
  | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)
  | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)

fun collectInterestingInfo(book: Book) -> Str =
  match book
    FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
    FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
    ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration"
    ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}"
    ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty"
    ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}"
    NonFictionBook(title, _, rating1, rating2) =>
      match rating1, rating2
        Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}"
        _, _ => "Non-fiction book \"{title}\" has mixed reviews"

fun describeFantasy() -> Str =
  collectInterestingInfo(FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: True))

fun main() =
  printLn(describeFantasy())

test "fantasy book with mythical creatures is called out"
  assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures"

test "fantasy book without mythical creatures is called out"
  book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: False)
  assert collectInterestingInfo(book) == "Fantasy book \"Plain Fantasy\" has no mythical creatures"

test "scifi book with space exploration theme is called out"
  book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration")
  assert collectInterestingInfo(book) == "Scifi book \"Starbound\" explores space exploration"

test "scifi book with other theme falls through to generic case"
  book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel")
  assert collectInterestingInfo(book) == "Scifi book \"Timeloop\" has theme time travel"

test "empty childrens book is handled via a when guard on pages"
  book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
  assert collectInterestingInfo(book) == "Childrens book \"Nothing\" is empty"

test "childrens book with pages reports its moral lesson"
  book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins")
  assert collectInterestingInfo(book) == "Childrens book \"The Tortoise\" teaches: patience wins"

test "non fiction book praised by both reviewers via nested deconstruction"
  book := NonFictionBook(
    title: "True Facts",
    pages: 120,
    rating1: Good(name: "Alice"),
    rating2: Good(name: "Bob"),
  )
  assert collectInterestingInfo(book) == "Non-fiction book \"True Facts\" praised by Alice and Bob"

test "non fiction book with a bad rating reports mixed reviews"
  book := NonFictionBook(
    title: "Shaky Claims",
    pages: 80,
    rating1: Good(name: "Alice"),
    rating2: Bad(name: "Carol"),
  )
  assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews"