Kleisli

An idiomatic, clean implementation of a few common useful monads in Ruby, written by Ryan Levick and me.

It aims to be idiomatic Ruby to use in real app, not a proof of concept.

In your Gemfile:

gem 'kleisli'

We would like to thank Curry and Howard for their correspondence.

Notation

For all its monads, Kleisli implements return (we call it lift instead, as return is a reserved keyword in Ruby) with convenience global methods (see which for each monad below).

Kleisli uses a clever Ruby syntax trick to implement the bind operator, which looks like this: >->. We will probably burn in hell for this.

Maybe monad

The Maybe monad is useful to express a pipeline of computations that might return nil at any point. user.address.street anyone?

>-> (bind)

require "kleisli"

maybe_user = Maybe(user) >-> user {
  Maybe(user.address) } >-> address {
    Maybe(address.street) }

# If user exists
# => Some("Monad Street")
# If user is nil
# => None()

# You can also use Some and None as type constructors yourself.
x = Some(10)
y = None()

fmap

require "kleisli"

# If we know that a user always has an address with a street
Maybe(user).fmap(&:address).fmap(&:street)

# If the user exists
# => Some("Monad Street")
# If the user is nil
# => None()

Try

The Try monad is useful to express a pipeline of computations that might throw an exception at any point.

>-> (bind)

require "kleisli"

json_string = get_json_from_somewhere

result = Try { JSON.parse(json_string) } >-> json {
  Try { json["dividend"].to_i / json["divisor"].to_i }
}

# If no exception was thrown:

result       # => #<Try::Success @value=123>
result.value # => 123

# If there was a ZeroDivisionError exception for example:

result           # => #<Try::Failure @exception=#<ZeroDivisionError ...>>
result.exception # => #<ZeroDivisionError ...>

fmap

require "kleisli"

Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).value

# If everything went well:
# => { :my => "json", :with => "symbolized keys" }
# If an exception was thrown:
# => nil

to_maybe

Sometimes it's useful to interleave both Try and Maybe. To convert a Try into a Maybe you can use to_maybe:

require "kleisli"

Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_maybe

# If everything went well:
# => Some({ :my => "json", :with => "symbolized keys" })
# If an exception was thrown:
# => None()

Either

The Either monad is useful to express a pipeline of computations that might return an error object with some information.

It has two type constructors: Right and Left. As a useful mnemonic, Right is for when everything went "right" and Left is used for errors.

Think of it as exceptions without messing with the call stack.

>-> (bind)

require "kleisli"

result = Right(3) >-> value {
  if value > 1
    Right(value + 3)
  else
    Left("value was less or equal than 1")
  end
} >-> value {
  if value % 2 == 0
    Right(value * 2)
  else
    Left("value was not even")
  end
}

# If everything went well
result # => Right(12)
result.value # => 12

# If it failed in the first block
result # => Left("value was less or equal than 1")
result.value # => "value was less or equal than 1"

# If it failed in the second block
result # => Left("value was not even")
result.value # => "value was not even"

fmap

require "kleisli"

result = if foo > bar
  Right(10)
else
  Left("wrong")
end.fmap { |x| x * 2 }

# If everything went well
result # => Right(20)
# If it didn't
result # => Left("wrong")

to_maybe

Sometimes it's useful to turn an Either into a Maybe. You can use to_maybe for that:

require "kleisli"

result = if foo > bar
  Right(10)
else
  Left("wrong")
end.to_maybe

# If everything went well:
result # => Some(10)
# If it didn't
result # => None()

Writer

The Writer monad is arguably the least useful monad in Ruby (as side effects are uncontrolled), but let's take a look at it anyway.

It is used to model computations that append to some kind of state (which needs to be a Monoid, expressed in the Kleisli::Monoid mixin) at each step.

(We've already implemented the Monoid interface for String, Array, Hash, Fixnum and Float for you.)

An example would be a pipeline of computations that append to a log, for example a list of strings.

>-> (bind)

require "kleisli"

writer = Writer([], 100) >-> value {
  Writer(["added 100"], value + 100)
} >-> value {
  Writer(["added 140 more"], value + 140)
} # => Writer(["added 100", "added 140 more"], 340)

log, value = writer.unwrap
log # => ["added 100", "added 140 more"]
value # => 340

fmap

require "kleisli"

writer = Writer([], 100).fmap { |value|
  value + 100
} # => Writer([], 200)

Future

The Future monad models a pipeline of computations that will happen in the future, as soon as the value needed for each step is available. It is useful to model, for example, a sequential chain of HTTP calls.

>-> (bind)

require "kleisli"

f = Future("myendpoint.com") >-> url {
  Future { HTTP.get(url.call) }
} >-> response {
  Future {
    other_url = JSON.parse(response.call.body)[:other_url]
    HTTP.get(other_url)
  }
} >-> other_response {
  Future { JSON.parse(other_response.call.body) }
}

# Do some other stuff...

f.await # => block until the whole pipeline is realized
# => { "my" => "response body" }

fmap

require "kleisli"

Future { expensive_operation }.fmap { |x| x * 2 }.await
# => result of expensive_operation * 2

Who's this

This was made by Josep M. Bach (Txus) and Ryan Levick under the MIT license. We are @txustice and @itchyankles on twitter (where you should probably follow us!).