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
# Classic OOP alternative to the DOP version in `dop_visitor.plum`: instead of
# one sealed `enum` + one exhaustive `match`, each book shape gets its own
# `type`, declares it implements the `Book` trait, and defines its own
# `collectInterestingInfo` method — the direct translation of the blog post's
# Java (sealed interface implemented by records, each overriding a method).
# Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
#
# One real limitation this example runs into: plum's `trait` is a
# compile-time structural check only ("does this type declare these
# methods with matching signatures?") — there is no vtable or runtime
# dispatch. A function can't take a bare `book: Book` parameter and call
# `book.collectInterestingInfo()` polymorphically across different concrete
# types, and there's no way to hold a heterogeneous list of `Book`s. So
# unlike the Java original (or the enum/match version), there's no single
# `describe(book: Book)` entry point here — each concrete type's method is
# called directly at its own call site.
#
# OOP vs. DOP (see the `enum`/`match` version in `dop_visitor.plum`):
#   - Adding a new variant (a new `type(Book) = ...`) is self-contained here
#     and doesn't touch existing types. In the DOP version it means adding
#     a new arm to every `match` on `Book` (caught by the checker's
#     exhaustiveness check if missed).
#   - Adding a new operation instead means adding a method to every `type`
#     here, with nothing enforcing that you updated all of them; the DOP
#     version adds one new function with one exhaustive `match`.
#   - So OOP fits best when shapes multiply and operations are stable; DOP
#     fits best when operations change often and the shapes are stable.
#     plum's lack of dynamic dispatch through a trait type (see above)
#     removes OOP's usual "call through one interface" benefit entirely,
#     which is a real cost specific to this language.

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

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

trait Book =
  collectInterestingInfo() -> Str

enum FantasyBook(Book) =
  | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)

  fun collectInterestingInfo(self) -> Str =
    if self.hasMythicalCreatures
      "Fantasy book \"{self.title}\" features mythical creatures"
    else
      "Fantasy book \"{self.title}\" has no mythical creatures"

enum ScifiBook(Book) =
  | ScifiBook(title: Str, pages: Int, theme: Str)

  fun collectInterestingInfo(self) -> Str =
    if self.theme == "space exploration"
      "Scifi book \"{self.title}\" explores space exploration"
    else
      "Scifi book \"{self.title}\" has theme {self.theme}"

enum ChildrensTaleBook(Book) =
  | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)

  fun collectInterestingInfo(self) -> Str =
    if self.pages == 0
      "Childrens book \"{self.title}\" is empty"
    else
      "Childrens book \"{self.title}\" teaches: {self.moralLesson}"

enum NonFictionBook(Book) =
  | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)

  fun collectInterestingInfo(self) -> Str =
    match self.rating1, self.rating2
      Good(name1), Good(name2) => "Non-fiction book \"{self.title}\" praised by {name1} and {name2}"
      _, _ => "Non-fiction book \"{self.title}\" has mixed reviews"

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

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 book.collectInterestingInfo() == "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 book.collectInterestingInfo() == "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 book.collectInterestingInfo() == "Scifi book \"Timeloop\" has theme time travel"

test "empty childrens book is handled via a plain if on pages"
  book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
  assert book.collectInterestingInfo() == "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 book.collectInterestingInfo() == "Childrens book \"The Tortoise\" teaches: patience wins"

test "non fiction book praised by both reviewers via nested match"
  book := NonFictionBook(
    title: "True Facts",
    pages: 120,
    rating1: Good(name: "Alice"),
    rating2: Good(name: "Bob"),
  )
  assert book.collectInterestingInfo() == "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 book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews"