Combining things in Ruby with Kleisli

Kleisli is a clean Ruby implementation of some structures we find naturally in the world of functional programming. In this post we'll explore how to effectively structure computations and pipelines using Kleisli in Ruby.

Functor

In the words of the Haskell documentation, a "Functor" can be mapped over. A simple example of this is a list. Many Ruby programmers will be familiar with code like my_list.map { ... }. Functor lets us extend this idea further.

First off, let's rewrite a "map" type function for a linked list structure and call it fmap.

require 'kleisli'

class Node < Kleisli::Functor
  def initialize(val, nextNode)
    @val, @nextNode = val, nextNode
  end

  def fmap(&f)
    # Look ma! No nil checks!
    Node.new(f.call(@val), @nextNode.fmap(&f))
  end
end

# ...

Node.new(1, Node.new(2, None())).fmap { |x| x + 1 }

# => #<Node
#      @val=2,
#      @nextNode=#<Node
#        @val=3,
#        @nextNode=None>>

We don't just fmap over lists though, we can map over fail-y values and other Functors:

Maybe(1).fmap { |x| x + 1 }.fmap { |x| x + 2 }
# => Some(4)

None().fmap { |x| + 1 }.fmap { |x| x + 2 }
# => None

So Functor is a pretty generic term involving the notion of "mapping" a function over a value.

Applicative Functor

Sometimes you'll hear references to "wrapping values" as an intuition for Functors. This can be a useful (but limited) metaphor. Applicative Functors let us "wrap" functions too. The * operator is used to combine things here using Kleisli:

Some(-> x, y { x + y }) * Some(1) * Some(2)
# => Some(3)

Introducing failures anywhere will cause the whole computation to fail:

# Apply a non existent function to two arguments
None() * Some(1) * Some(2)
# => None

# Apply a function to some missing arguments:
Some(-> x, y { x + y }) * None() * Some(2)
# => None

Once you get used to it, this provides an alternative to manual error-checking and nil-checking.

Monads

Monads are one step further. With a Monad we can build a pipeline of computations. We'll use the Either monad - which uses a Left constructor to indicate failure and a Right constructor to indicate success. Let's find the average of the even numbers in a list, then format it into a message:

def average_of_evens(list)
  Right(list.find_all(&:even?)) >-> subList {
    size = subList.count
    if size.zero?
      Left("There are no evens!")
    else
      sum = subList.inject(:+).to_f
      Right(sum / size)
    end
  } >-> result {
    Right("The average of the evens is: #{result}")
  }
end

average_of_evens([1,3,5])
# => Left("There are no evens!")

average_of_evens([1,2,3,4,5,6])
# => Right("The average of the evens is: 4.0")```

Caveats

Ruby won't stop you writing code such as:

None() + 1
# => NoMethodError: undefined method `+' for None:Kleisli::Maybe::None

When what you wanted was fmap:

None().fmap { |x| x + 1 }
# => None

For that we'd need a type-checker or some other sophisticated static analysis. Additionally writing programs with Kleisli could not be considered idiomatic. However it is still a valid alternative in certain contexts where manual error checking and function composition is tiresome and error-prone.