
NAME
Monad - Parametric-typed monads for Raku
SYNOPSIS
use Monad::Maybe;
use Monad::Either;
# Type-parameterized: compile-time checks on the contained value
my Monad::Maybe[Int] $n = Monad::Maybe[Int].some(42);
say $n.unwrap; # 42
my $err = Monad::Maybe[Int].some('x'); # throws: Str isn't Int
# Or untyped — everything defaults to Any
my $m = Monad::Maybe.some([1, 2, 3]);
say $m.is-some; # True
# Lift a possibly-absent value, then eliminate it totally
my %ages = alice => 30;
say Monad::Maybe.from-defined(%ages<bob>).get-or-else(0); # 0
# Infix operators
my $doubled = Monad::Maybe[Int].some(5) >>- { $_ * 2 };
say $doubled.unwrap; # 10
DESCRIPTION
Monad is a small collection of parametric-typed monads for Raku: Maybe, Either, List, Writer, Reader, and State. The value-carrying monads accept a type parameter on their payload, giving you compile-time checking and self-documenting signatures:
sub load-user(Int $id --> Monad::Maybe[User]) { ... }
sub parse(Str $raw --> Monad::Either[Str, AST]) { ... }
The typed forms are opt-in. Legacy Monad::Maybe.some(42) (without the square brackets) still works — every type parameter defaults to Any.
WHY
Raku already has Nil and Failure for representing absent-or-errored values, but both blur the line between "no value" and "something went wrong". Monads give you a structural answer:
Maybe[T] — a T is either present (Some) or absent (None). Unambiguous.
Either[L, R] — a value is either a success (Right R) or a failure (Left L), both typed.
List[T] — a sequence of Ts, with bind as flatMap.
Writer[A, W] — a value plus an accumulated log.
Reader / State — computations parameterized by an environment.
The payoff: your function signatures express optionality and error handling at the type level, so consumers have to handle both cases explicitly and can chain them fluently with map and bind.
INSTALLATION
zef install Monad
UPGRADING TO 0.3
Version 0.3.0 is a breaking release. It exists to make Maybe and Either behave the way their counterparts do in every other functional language, so that a reader coming from Haskell or Scala can predict what every method does. Four things changed:
Maybe.map no longer collapses a Some when the mapped function returns an undefined value — map is a functor map, and functor maps do not change the shape of the container. Spell the old behaviour $m.bind({ Monad::Maybe.from-defined(f($_)) }) or $m.map(&f).filter(*.defined) .
Maybe.unit is now total: unit(Nil) is a Some. A pure that inspects its argument breaks the left-identity law. The old lifting behaviour is now called from-defined, which is what you want for lookups.
Maybe.unwrap dies on a None instead of returning Nil — it is fromJust, partial and loud. Use get-or-else or fold when you need a total eliminator, or .value for the raw attribute.
Either.unwrap is gone. It returned whichever side was there, so it silently handed you an error message where you wanted a result. Use unwrap-right, unwrap-left, get-or-else, fold, or .value.
Both types also gained a set of standard combinators: get-or-else, fold, sequence and traverse on both; filter, from-defined and to-either on Maybe; map-left, bimap, swap and to-maybe on Either.
Nothing else changed: constructors, bind, the marker roles, the infix operators, parametric usage and smart-matching all behave as they did in 0.2. The per-module Pod has a full MIGRATION section with before/after snippets.
INFIX OPERATORS
Two exported operators keep chains readable:
my $result = Monad::Maybe[Int].some(3)
>>- { $_ * 2 } # Some(6)
>>= -> $v { $v > 5 ?? Monad::Maybe[Int].some($v) !! Monad::Maybe[Int].none }
>>- { $_ + 1 }; # Some(7)
THE MONADS
Monad::Maybe
Represents a value that may or may not be present. Parameterized by the type of the contained value. Semantics follow Haskell's Maybe and Scala's Option: map is a functor map that never changes the container's shape, unit is a total pure, and unwrap is the partial fromJust that dies when there is nothing there.
use Monad::Maybe;
my Monad::Maybe[Str] $user = Monad::Maybe[Str].some('alice');
my Monad::Maybe[Str] $none = Monad::Maybe[Str].none;
say $user.is-some; # True
say $user.value; # alice
say $none.is-none; # True
say $none.unwrap; # dies — use get-or-else or fold instead
# Build from a value that may be undefined
my %known = alice => 'admin';
say Monad::Maybe.from-defined(%known<bob>).is-none; # True
# Chain with map / bind / filter
my $shout = $user.map({ .uc });
say $shout.value; # ALICE
say $user.filter(* eq 'bob').is-none; # True
# Total eliminators — no way to forget the empty case
say $none.get-or-else('anon'); # anon
say $user.fold(none => { 'anon' }, some => -> $u { $u }); # alice
# Convert, and work with collections of Maybes
say $none.to-either('no user').is-left; # True
say Monad::Maybe.sequence([$user, $none]).is-none; # True
# Type mismatch throws at construction:
try {
Monad::Maybe[Int].some('not an int');
CATCH { default { say "rejected: {.message}" } }
}
Full method-by-method documentation, including the 0.2 → 0.3 migration notes, lives in the Monad::Maybe Pod.
Monad::Either
Represents a value that is one of two types. Conventionally Left carries an error and Right carries success. Parameterized by the left and right types independently.
use Monad::Either;
sub parse-int(Str $s --> Monad::Either[Str, Int]) {
$s ~~ /^ (\d+) $/
?? Monad::Either[Str, Int].right(+$0)
!! Monad::Either[Str, Int].left("'$s' is not a number");
}
given parse-int('42') {
when .is-right { say "got: {.unwrap-right}" } # got: 42
when .is-left { say "error: {.unwrap-left}" }
}
# bind skips on Left, chains on Right
my $result = parse-int('10')
>>= -> $n { Monad::Either[Str, Int].right($n * 2) }
>>= -> $n { $n > 100
?? Monad::Either[Str, Int].left('too big')
!! Monad::Either[Str, Int].right($n) };
say $result.gist; # Right(20)
# Total eliminators, and access to the Left side
say parse-int('x').get-or-else(0); # 0
say parse-int('x').map-left(*.uc).gist; # Left('X' IS NOT A NUMBER)
say parse-int('x').fold(
left => -> $e { "failed: $e" },
right => -> $v { "ok: $v" },
); # failed: 'x' is not a number
# Collections: the first Left wins, unchanged
say Monad::Either.traverse(<1 2 x>, &parse-int).gist; # Left('x' is not a number)
Every chaining operation is right-biased. There is deliberately no side-blind unwrap: ask for unwrap-right, unwrap-left, get-or-else, fold, or the raw .value accessor. Full method-by-method documentation, including the 0.2 → 0.3 migration notes, lives in the Monad::Either Pod.
Monad::List
A sequence monad. bind (aka flatMap) applies a function that returns a List and flattens one level.
use Monad::List;
my $lst = Monad::List[Int].of(1, 2, 3);
# Map
my $doubled = $lst.map({ $_ * 2 });
say $doubled.values.List; # (2 4 6)
# flatMap
my $pairs = $lst.bind(-> $n {
Monad::List[Int].of($n, $n * 10)
});
say $pairs.values.List; # (1 10 2 20 3 30)
Monad::Writer
Carries a value alongside an accumulated log. Parameterized by the value type and the log type (defaulting to Str).
use Monad::Writer;
my $w = Monad::Writer[Int, Str].unit(5);
my $logged = $w.tell('starting with 5, ')
.map({ $_ * 2 })
.tell('doubled to ')
.bind(-> $v {
Monad::Writer[Int, Str].new(value => $v + 1, logs => "added one = {$v + 1}")
});
say $logged.value; # 11
say $logged.logs; # starting with 5, doubled to added one = 11
For non-string logs, subclass and override _combine:
class ArrayLogWriter is Monad::Writer {
has @.logs;
method _combine($a, $b) { [|$a, |$b] }
}
Monad::Reader and Monad::State
Computational monads for carrying an environment (Reader) or threading a state through a pipeline (State). These aren't parameterized — the underlying computation is a closure, so type parameters would only be documentation.
use Monad::State;
# Counter-style state manipulation
my $pipeline = Monad::State.get
>>= -> $n { Monad::State.put($n + 1) }
>>= -> $ { Monad::State.put(10) }
>>= -> $ { Monad::State.get };
my ($val, $final-state) = $pipeline.run(0);
say "value: $val, state: $final-state"; # value: 10, state: 10
WRITING YOUR OWN
Subclass Monad (the base class) to define your own. You must implement bind, map, and unit:
use Monad;
class MyMonad is Monad {
has $.value;
method bind(&f) { f($.value) }
method map(&f) { self.new(value => f($.value)) }
method unit($v) { self.new(value => $v) }
}
For a parametric type, use a role instead:
role MyMonad[::T = Any] is Monad {
has T $.value;
method bind(&f) { f($.value) }
method map(&f) { self.new(value => f($.value)) }
method unit($v) { self.new(value => $v) }
}
# Usage:
my $m = MyMonad[Int].new(value => 42);
IMPLEMENTATION NOTES
Parametric monads are implemented as Raku roles, not classes — Raku doesn't support parametric classes. Role auto-punning means:
$x ~~ Monad::Maybe works against the bare role name regardless of whether you constructed it with a type parameter.
isa-ok $x, Monad::Maybe likewise.
Class-method-style calls (Monad::Maybe.some(42)) work because Raku auto-puns parametric roles when you invoke them.
Variants are distinguished by a marker role mixed into each value, and the constructors always build from the named parameterization rather than from the invocant — otherwise calling a constructor on an existing value would stack a second marker on top of the first. The visible consequence is that .^name reports the parameterization explicitly (Monad::Maybe[Any]+{Monad::Maybe::Some}), and that every value of a given parameterization has exactly one type no matter how it was built, so two structurally identical Maybes compare equal under is-deeply. Use .gist for display and ~~ / is-some / is-left for dispatch; don't match on .^name.
Monad::State and Monad::Reader remain plain classes because the underlying run callable is type-erased (it's a closure Raku can't inspect). Parameterizing them would only add documentation noise.
Some State/Reader class methods (like put, get, modify) intentionally live on a non-parametric class to avoid dispatch collisions with Raku's built-in put and get on role type objects.
AUTHOR
Matt Doughty matt@apogee.guru
COPYRIGHT AND LICENSE
Copyright 2024–2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.