monads

A small functional toolkit: Maybe, Either and a cons-list, with the maps, binds, operators and traversals to chain them.

import monads

monads is written in Bern itself - every function below is an ordinary Bern function you could read in lib/monads.brn. It is built on three algebraic data types, which (since Bern 2.2) compare with == and may be mixed inside a list.

The types

adt Maybe  = Just Any | Nothing
adt Either = Left Any | Right Any
adt List   = Nil | Cons Any List

Maybe models a value that may be absent - Just(x) or Nothing() - so "no result" is explicit instead of a sentinel like -1 or null. Either models one of two outcomes, by convention Left(e) for a failure that carries information and Right(x) for a success. List is a classic linked list for code that wants to pattern-match the structure directly; list_from / list_to bridge it to Bern's built-in lists.

Nullary constructors are written with empty parentheses, both to build and to match: Nothing(), Nil().

Maybe

is_just(maybe) → boolean
is_nothing(maybe) → boolean

Test which shape a Maybe is.

is_just(Just(5))
-- Output: true
is_nothing(Nothing())
-- Output: true
from_maybe(default, maybe) → value

Unwrap a Just, or fall back to default when it is Nothing.

from_maybe(0, Just(42))
-- Output: 42
from_maybe(0, Nothing())
-- Output: 0
maybe(default, function, maybe) → value

Collapse a Maybe to a plain value: apply function to the contents of a Just, otherwise return default.

maybe(-1, \x -> x * 2, Just(10))
-- Output: 20
maybe(-1, \x -> x * 2, Nothing())
-- Output: -1
map_maybe(function, maybe) → maybe

Transform the value inside a Just; a Nothing passes through untouched.

map_maybe(\x -> x + 1, Just(7))
-- Output: Just(8)
map_maybe(\x -> x + 1, Nothing())
-- Output: Nothing()
bind_maybe(maybe, function) → maybe

Chain a Maybe-returning function. The chain short-circuits to Nothing as soon as any step produces one.

bind_maybe(Just(4), \x -> Just(x * x))
-- Output: Just(16)
bind_maybe(Nothing(), \x -> Just(x))
-- Output: Nothing()

Either

is_left(either) → boolean
is_right(either) → boolean

Test which side an Either holds.

is_right(Right(1))
-- Output: true
is_left(Left("bad"))
-- Output: true
either(on_left, on_right, either) → value

Collapse an Either by handling both sides with a function each.

either(\e -> "error: " + e, \x -> "ok", Left("bad"))
-- Output: "error: bad"
either(\e -> 0, \x -> x, Right(9))
-- Output: 9
map_either(function, either) → either

Map over the success (Right) side; a Left passes through unchanged.

map_either(\x -> x + 1, Right(9))
-- Output: Right(10)
map_either(\x -> x + 1, Left("e"))
-- Output: Left(e)
map_left(function, either) → either

Map over the failure (Left) side; a Right passes through unchanged.

map_left(\e -> e + "!", Left("oops"))
-- Output: Left(oops!)
bind_either(either, function) → either

Chain on the Right side; the first Left short-circuits.

bind_either(Right(2), \x -> Right(x * 10))
-- Output: Right(20)
bind_either(Left("stop"), \x -> Right(x))
-- Output: Left(stop)
from_right(default, either) → value
from_left(default, either) → value

Unwrap one side, falling back to default for the other.

from_right(0, Right(5))
-- Output: 5
from_right(0, Left("x"))
-- Output: 0
either_to_maybe(either) → maybe
maybe_to_either(error, maybe) → either

Convert between the two. A Left becomes Nothing and a Right becomes Just; going the other way, a Nothing becomes Left(error).

either_to_maybe(Right(3))
-- Output: Just(3)
maybe_to_either("none", Nothing())
-- Output: Left(none)

Generic bind & operators

bind and mmap dispatch on the constructor, so they work uniformly over both Maybe and Either - which is what lets the operators stay type-agnostic.

bind(monad, function) → monad

Generic monadic bind for a Maybe or an Either.

bind(Just(5), \x -> Just(x + 1))
-- Output: Just(6)
bind(Right(5), \x -> Right(x + 1))
-- Output: Right(6)
mmap(function, monad) → monad

Generic functor map for a Maybe or an Either.

mmap(\x -> x * 3, Just(4))
-- Output: Just(12)
function <$> monad → monad

Operator form of mmap: map a plain function over a monadic value.

(\x -> x + 100) <$> Just(1)
-- Output: Just(101)
monad >>= function → monad

Operator form of bind: feed a monadic value into a monadic function. Left-associative, so chains read top to bottom.

Just(5) >>= \x -> Just(x + 1) >>= \y -> Just(y * 2)
-- Output: Just(12)

Left("stop") >>= \x -> Right(x)
-- Output: Left(stop)

The List ADT

A linked list built from Cons(head, tail) and Nil(). Bern already has a fast built-in list ([1, 2, 3]); this ADT is for teaching the shape and for code that pattern-matches the structure. The bridges convert to and from built-in lists.

list_from(builtin_list) → List
list_to(List) → builtin_list

Convert between a built-in list and the Cons/Nil ADT.

list_to(list_from([1, 2, 3]))
-- Output: [1, 2, 3]
list_map(function, List) → List

Apply a function to every element.

list_to(list_map(\x -> x * x, list_from([1, 2, 3])))
-- Output: [1, 4, 9]
list_foldr(function, accumulator, List) → value

Reduce from the right; function receives (element, accumulator).

list_foldr(\a, b -> a + b, 0, list_from([1, 2, 3, 4]))
-- Output: 10
list_length(List) → int
list_length(list_from([4, 5, 6, 7]))
-- Output: 4
list_append(List, List) → List
list_to(list_append(list_from([1, 2]), list_from([3, 4])))
-- Output: [1, 2, 3, 4]
list_reverse(List) → List

Reverse via a tail-recursive accumulator - O(n) with O(1) prepends.

list_to(list_reverse(list_from([1, 2, 3])))
-- Output: [3, 2, 1]

Traversals

Walk a built-in list, run an effectful step on each element, and collect the results - short-circuiting on the first failure. These rely on a list being able to hold both constructors of one ADT (e.g. [Just(1), Nothing()]), which Bern 2.2 allows.

sequence_maybe(list_of_maybes) → maybe

Turn a list of Maybes into a Maybe of a list: Just of all the values if every element is Just, otherwise Nothing.

sequence_maybe([Just(1), Just(2), Just(3)])
-- Output: Just([1, 2, 3])
sequence_maybe([Just(1), Nothing(), Just(3)])
-- Output: Nothing()
traverse_maybe(function, list) → maybe

Map a Maybe-returning function across a list, failing fast on the first Nothing.

traverse_maybe(\x -> Just(x + 1), [1, 2, 3])
-- Output: Just([2, 3, 4])
sequence_either(list_of_eithers) → either
traverse_either(function, list) → either

The Either versions: Right of all the values, or the first Left encountered.

sequence_either([Right(1), Left("oops"), Right(3)])
-- Output: Left(oops)

Putting it together

The pieces compose into pipelines that never crash on a missing or invalid value. Here a couple of inputs are parsed, validated, and combined - any failure collapses the whole chain to Nothing:

A computation that might fail

import monads

def safe_div(a, b) do
    if (b == 0) then
        return Nothing()
    else
        return Just(a / b)
    end
end

result = Just(100)
    >>= \x -> safe_div(x, 5)
    >>= \y -> safe_div(y, 2)

from_maybe(-1, result)
-- Output: 10   (100 / 5 = 20, then 20 / 2 = 10)

from_maybe(-1, Just(100) >>= \x -> safe_div(x, 0))
-- Output: -1   (the division by zero short-circuits)

And a traversal that validates a whole list at once, keeping the why on failure with Either:

Validating every item

import monads

def check_positive(n) do
    if (n > 0) then
        return Right(n)
    else
        return Left("not positive: " + n)
    end
end

from_right([], traverse_either(check_positive, [3, 8, 5]))
-- Output: [3, 8, 5]

from_left("?", traverse_either(check_positive, [3, 0, 5]))
-- Output: "not positive: 0"