plum
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"