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 (legacy) — everything defaults to Any
my $m = Monad::Maybe.some([1, 2, 3]);
say $m.is-some;                         # True

# 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

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.

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;      # Nil

# Chain with map / bind
my $shout = $user.map({ .uc });
say $shout.value;      # ALICE

# Type mismatch throws at construction:
try {
    Monad::Maybe[Int].some('not an int');
    CATCH { default { say "rejected: {.message}" } }
}

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)

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:

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.