Skip to content

Chaining Monads

Every monad in this library shares the same handful of operations. Learn them once and they work for Maybe, Either, List, and the rest.

The guiding idea: operations applied to an "empty" or "error" variant (Nothing, Left, LeftException) are skipped, carrying the empty/error state through the chain untouched.

fmap

fmap transforms the value inside the monad, returning a monad of the same kind. It does not affect Nothing or Left.

crystal
value = Monads::Just.new(5)
  .fmap(->(x : Int32) { x.to_s })
  .fmap(->(x : String) { x + "12" })
  .fmap(->(x : String) { x.to_i })
  .value! # => 512

A block form is also available:

crystal
Monads::Just.new(5).fmap { |x| x * 2 } # => Just(10)

bind

bind transforms the value into a whole new monad. Use it when your transformation itself returns a monad, to avoid nesting (Just(Just(...))). Like fmap, it does not affect Nothing or Left.

crystal
value = Monads::Just.new(5)
  .bind(->(x : Int32) { Monads::Try(Int32).new(-> { x / 0 }).to_maybe })

value # => Nothing(Int32)

The | operator is an alias for bind:

crystal
Monads::Just.new(5) | ->(x : Int32) { Monads::Just.new(x + 1) } # => Just(6)

fold

fold pattern-matches on the two variants of a monad, applying a different function to each and returning the result.

Either

For Either, fold takes a function for the success case (Right) and one for the error case (Left):

crystal
Monads::Right(String, Int32).new(42).fold(
  ->(value : Int32) { "Success: #{value}" },
  ->(error : String) { "Error: #{error}" }
) # => "Success: 42"

Monads::Left(String, Int32).new("Not found").fold(
  ->(value : Int32) { "Success: #{value}" },
  ->(error : String) { "Error: #{error}" }
) # => "Error: Not found"

A block version applies the block to Right values and raises for Left:

crystal
Monads::Right(String, Int32).new(21).fold { |x| x * 2 } # => 42

Maybe

For Maybe, fold takes a function for Just and one (taking no argument) for Nothing:

crystal
Monads::Just.new(42).fold(
  ->(x : Int32) { "Got: #{x}" },
  -> { "Nothing here" }
) # => "Got: 42"

Monads::Nothing(Int32).new.fold(
  ->(x : Int32) { "Got: #{x}" },
  -> { "Nothing here" }
) # => "Nothing here"

A block version applies the block to Just values and raises for Nothing:

crystal
Monads::Just.new(21).fold { |x| x * 2 } # => 42

Unwrapping the value

How you get a value back out depends on whether you want to assume success or provide a fallback:

  • value! — returns the wrapped value. Use only when you know the variant.
  • value_or(default) — returns the value, or default for the empty/error case.
  • value_or(->(err) { ... }) — same, but computes the fallback from the error.
  • map_or(default, ->(x) { ... }) — transforms the value, or returns default.
crystal
Monads::Just.new(5).value_or(0)        # => 5
Monads::Nothing(Int32).new.value_or(0) # => 0

Monads::Just.new(5).map_or(0, ->(x : Int32) { x * 2 })        # => 10
Monads::Nothing(Int32).new.map_or(0, ->(x : Int32) { x * 2 }) # => 0

The >> operator

>> sequences two monads, discarding the left value and returning the right one — useful when you only care about the second computation:

crystal
Monads::Just.new(1) >> Monads::Just.new(2) # => Just(2)

Try and Task

Try and Task are not chained directly. Convert them into a Maybe or an Either first with to_maybe / to_either, then chain as usual.

Released under the MIT License.