Rand Stats

Monad

zef:apogee

Actions Status

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:

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:

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:

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.