Skip to content

Avoiding nil with Maybe

Maybe turns "this value might be missing" into an explicit type, so the compiler and the API make you handle the empty case instead of risking a nil at runtime.

Wrapping a lookup

Suppose you look something up that may not exist. Return a Maybe instead of a nilable value:

crystal
USERS = {1 => "Ada", 2 => "Alan"}

def find_user(id : Int32) : Monads::Maybe(String)
  name = USERS[id]?
  name ? Monads::Just.new(name) : Monads::Nothing(String).new
end

Composing transformations safely

Each fmap runs only when a value is present. A Nothing flows straight through:

crystal
find_user(1)
  .fmap(->(name : String) { name.upcase })
  .value_or("UNKNOWN") # => "ADA"

find_user(99)
  .fmap(->(name : String) { name.upcase })
  .value_or("UNKNOWN") # => "UNKNOWN"

Chaining lookups with bind

When a follow-up step also returns a Maybe, use bind so the result does not nest, and the chain short-circuits on the first Nothing:

crystal
GREETINGS = {"Ada" => "Hello, Ada!"}

def greeting_for(name : String) : Monads::Maybe(String)
  msg = GREETINGS[name]?
  msg ? Monads::Just.new(msg) : Monads::Nothing(String).new
end

find_user(1)
  .bind(->(name : String) { greeting_for(name) })
  .value_or("No greeting") # => "Hello, Ada!"

find_user(2)
  .bind(->(name : String) { greeting_for(name) })
  .value_or("No greeting") # => "No greeting"

Picking a fallback with or

or lets you fall back to another Maybe:

crystal
find_user(99)
  .or(Monads::Just.new("Guest"))
  .value! # => "Guest"

Released under the MIT License.