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