Demystifying the IO Monad for Rubyists

Posted on May 28, 2017 by Tina Wuest

There is a terrible cycle which exists in the lifecycle of functional programmers. Monads are confusing at first so the programmer will read many articles explaining how monads are actually easy—which will fail to achieve any pedagogical goals due to the fact that the article is filled with self-referential language, leaving the programmer even more confused. Eventually, monads will click (like pointers, or pass-by-reference, or so many other concepts tend to eventually do), and the programmer will be happy and productive. Finally, after a few years working with this understanding, and after several discussions helping other programmers to better understand monadic abstractions, the programmer will write something explaining how monads are actually just delicious collections of meat, cheese, and vegetables wrapped in bread. The cycle has completed, and it will continue.

This article will complete my contract with the dark lords of functional programming, and complete the cycle for myself. I hope it will be useful to you, dear reader, by some meaning of the word.

I’ve been writing Haskell for a few years - I had a late start due to a problem with the available materials on the language: there was a wealth of introductory material, which glossed over details, and there was a wealth of advanced material (not to mention high-usage codebases such as XMonad) from which to learn advanced topics, but the path to take a programmer from finishing the former materials to digging in to the latter was difficult to discern. My first adventure with Haskell in 2007 ended abruptly because even though I could ostensibly write working programs, I didn’t fully understand a lot of why I needed to do things the way I did. This bothered me. So I kept on writing software in the languages comfortable to me - Ruby, Perl, C - and picked up Erlang a few years later. It wouldn’t be until 2013 that a slightly more determined version of me would dig in to figure out what I was missing with Haskell after being spurred on by a brief foray into ML.

So now, I’d like to set some expectations for the reader. This is a Ruby article first and foremost, and any concepts introduced will be explained with Ruby code. There will be examples that lean on concepts used frequently in Haskell, but I promise that I’ll make sure that any of them are explained in Ruby terms as well. This post is going to be long and will cover a lot of information, but it’s going to be painting with a very broad brush. The aim is to be sufficient rather than comprehensive, and to act as a jumping-off point for readers who wish to learn more. A reader with an intermediate understanding of Ruby will hopefully gain a better understanding of both Ruby and Haskell, as well as how concepts core to programming in Haskell can be used to write more maintainable Ruby programs. For the sake of accessability, in code snippets will prefer the long-form block passing syntax to the more idiomatic &:symbol syntax, even where it’s the obvious choice.

Let’s Talk About Arrays

So let’s jump straight into it! Arrays need no introduction; working with collections of data is as core to programming as anything. For the sake of simplicity, we’ll constrain our examples to arrays containing a single type of object, even though ruby allows for mixed-type arrays. One of the most common ways to manipulate data with Arrays is map:

[1, 2, 3, 4].map { |int| int.to_s } # => ['1', '2', '3', '4']

This is obvious and encompasses a huge number of our needs when it comes to working with lists of data. We can map functions over our collection, doing work on each item. This type of pattern tends to have a fairly common shape, but one thing we tend to lack in the Ruby world is a concrete way to describe the way patterns look. To this end, I’m going to introduce the way we handle this problem in Haskell: with type signatures. If we were to describe this particular map, we’d say it looks something like this:

[Fixnum] -> [String]

So far, this seems mostly clear. “Take an array of Fixnums and translate it to an array of Strings.” This is nice, since we can talk about this particular call to map — but we can only talk about this call to map. So let’s replace our specific types with generic type names:

[a] -> [b]

This is a better description of the function which map expects: we’ll take an array of some type, and transform it to an array of some type (which might be the same, but which is allowed to be different). Next let’s isolate this function with parentheses and add the arrays we’ll start and end with to our type signature, leaving us with something we can read as “take a function from a to b, and an array of a, and we’ll end up with an array of b.”

(a -> b) -> [a] -> [b]

So now that we can talk about the shape of functions, let’s re-visit map. Since being able to map functions over collections of data is so useful, it would be nice if we could talk about types of data which share this principle. That way we could talk about any sort of data container, whether it’s an array, a hash, a tree, or just a box into which we put a single value. If we can map functions over the contained data, they’re a member of this group. This group of types has a name - these types are Functors.

Just to make sure we don’t get surprised, let’s set a few ground rules for our new Functor friends:

  • Mapping an identity function (that is, a function which returns the value passed to it) over a Functor should not result in a change to the data the Functor encapsulates. In Ruby, all objects have an itself method which acts as an identity function.
  • Mapping f(g(x)) over a Functor should produce the same result as mapping g(x) followed by mapping f(x) over the Functor.

These two rules boil down to one extremely important point: Functors should never modify the data they encapsulate. After all, if we weren’t able to trust that our Arrays, for example, didn’t modify our data every time we interacted with them, we’d have to find some other way to deal with collections of data.

Let’s Talk Maybe

Moving beyond arrays, one pattern we see come up frequently in our Ruby is the case where a given call might return a value, or it may return nil. There are a lot of strategies of coping with this ranging from explicitly checking if a given object is nil before doing work, to prefixing every computation with an explicit cast if a nil might have snuck in. This tends to create a bit of uncertainty in our codebases, which often stands in stark contrast to the confidence which Ruby tends to inspire otherwise. Looking at the following example:

highest.rb
# Sort an array of comparable values in descending order, then take the highest
def highest_number(array)
  sorted = array.sort { |x, y| y <=> x }
  sorted.first
end

It’s difficult for us to figure out how to describe this with a type signature. We expect an array of comparable elements, but this method can return nil if an empty array is passed to it. So even if we constrict our view to Fixnums, Fixnums aren’t nil and nil certainly isn’t a Fixnum. In order to allow for us to describe this function, let’s imagine a new encapsulation: something which might contain a value, or it might contain nothing at all. We can call this Maybe. With the use of this new data type, we can describe the highest_number method:

[Comparable] -> Maybe Comparable

Now we’re talking! But how can we actually implement this Maybe type so that we don’t have to continue with nil checks? Before continuing, please feel encouraged to try and create a new Maybe Functor we can use to allow for this sort of encapsulation. Make sure that the class contains a method to map over the encapsulated value (I am calling this fmap for “Functor map”); it might be helpful to implement an inspect method for ease of use when it comes to working with the type in your repl of choice. A relatively simple solution might look like this:

maybe.rb
# Encapsulate a value which might be nil, but upon which we want to
# conditionally perform computations
class Maybe
  def initialize(val)
    @_val = val
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result wrapped in a shiny new context
  def fmap
    return self if @_val.nil?
    self.class.new(yield @_val)
  end

  # Convenience function for working with Maybe in irb/pry
  def inspect
    return 'Nothing' if @_val.nil?
    'Just ' + @_val.inspect
  end
end

Now we can use this Maybe construct to abstract away any nil checks in our code! Programmers with some experience with Rails will probably notice the similarity between our Maybe and the idea which Rails introduces to objects with try:

# Rails
array_of_numbers
  .detect { |n| n.even? }
  .try    { |n| n + 1 }
  .try    { |n| n.to_s }

# Maybe
Maybe.new(array_of_numbers.detect { |n| n.even? })
     .fmap { |n| n + 1 }
     .fmap { |n| n.to_s }

Let’s Talk About Usefulness

So far we’ve made some decent progress building a structure which will let us abstract away the busy work of stressing over whether an object might be nil. Let’s go ahead and rewrite highest.rb so that we can return the answer wrapped in our shiny new Maybe functor:

highest_maybe.rb
# Sort an array of comparable values in descending order, then wrap the highest
# in a Maybe context to avoid explicit nil handling
def highest_number(array)
  sorted = array.sort { |x, y| y <=> x }
  Maybe.new(sorted.first)
end

Nifty. We can test it out now:

irb :001 > highest_number([1, 2, 3]).fmap { |n| n**2 }.fmap { |n| n.to_s }
 => Just "9"
irb :002 > highest_number([]).fmap { |n| n**2 }.fmap { |n| n.to_s }
 => Nothing

This is exactly what we expected to have happen. No more checks to see if something’s nil, no more worrying about NoMethodError since we let a nil slip in somewhere we didn’t expect. We have become just a little bit more confident with the code that we write, and that’s a delightful feeling.

There’s a problem that lurks under the surface here waiting to rear its head, though. You might have sensed it nagging at your sensibilities already. What if we have another method, which returns values wrapped in Maybe?

clean_square_root.rb
# If a number is a perfect square, return its square root.  Otherwise, return
# Nothing
def clean_square_root(number)
  square = Math.sqrt(number)
  clean  = square.to_i if square == square.to_i
  Maybe.new(clean)
end

In situations where we’re in an all or nothing world and only perfect squares will satisfy us, we might take a path that looks like this. “Give me an integer square root or give me nothing at all,” we’ll shout - and our code will obey. But what happens when we map it over our code that gives us the highest number in a list?

irb :001 > highest_number([1, 4, 25, 9, 2]).fmap { |n| clean_square_root(n) }
 => Just Just 5
irb :002 > highest_number([1, 4, 26, 9, 2]).fmap { |n| clean_square_root(n) }
 => Just Nothing
irb :003 > highest_number([]).fmap { |n| clean_square_root(n) }
 => Nothing

This isn’t stellar. Now we might have to contend with Just Just 5, or Just Nothing, or… just Nothing which is not Just Nothing but is merely Nothing at all. This is going to get messy. We are going to end up writing code that looks like this:

five_layer_burrito.fmap do |l1|
  l1.fmap do |l2|
    l2.fmap do |l3|
      l3.fmap do |l4|
        l4.fmap { |l5| l5 + "I have five mouths and I must scream" }
      end
    end
  end
end

Okay, this is very bad. It insults every sensibility we tend to have as programmers. So we need to write something that will avoid adding layers of Functors to our Functors. Something that has a shape like this:

Functor a -> (a -> Functor b) -> Functor b

If this makes you think of the type signature for map, you’re on the right track. Instead of keeping things specific to the Array class by wrapping everything in square brackets, we’ve generalized things by making it clear we’re talking about any Functor here (remember, we can think of Array as a functor if we squint and think of map as fmap). The big difference here is that instead of un-wrapping and re-wrapping the contained value in our context, we are unwrapping the value and expecting the function we’re passed to return a wrapped value. Let’s call this new method bind. This might take a while to find a solution that feels right, but it’s worth the effort.

maybe_stackable.rb
# Encapsulate a value which might be nil, but upon which we want to
# conditionally perform computations
class Maybe
  def initialize(val)
    @_val = val
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result wrapped in a shiny new context
  def fmap
    return self if @_val.nil?
    self.class.new(yield @_val)
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result.  The block passed to bind is expected to wrap the result in a new
  # context for us
  def bind
    return self if @_val.nil?
    yield @_val
  end

  # Convenience function for working with Maybe in irb/pry
  def inspect
    return 'Nothing' if @_val.nil?
    'Just ' + @_val.inspect
  end
end

This is an exceedingly simplistic approach, but fairly consistent with Ruby idioms. We trust the block passed to our method to return the appropriate type of object at the end of the day, and as long as we build systems which behave appropriately, we’re going to be just fine here. As with our definition of Functors, we would do well to lay down some ground rules, though: just to make sure everyone gets along nicely.

  • We should have a function which creates a new structure, which is equivalent to an identity function in this context.
  • Our new method bind may care about the order which computations occur in, but it should not care about the way they’re nested.

There’s nothing that sounds super unreasonable, here. It turns out that we already have a function which creates our context for us: new! And it turns out that new works as an identity function in the context of bind:

irb :001 > clean_square_root(25).bind { |n| Maybe.new(n) } # This is called the "right" identity
 => Just 5
irb :002 > Maybe.new(25).bind { |n| clean_square_root(n) } # This is called the "left" identity
 => Just 5

We also can show that nesting doesn’t impact the result so long as the order of computations stays consistent (this was an important point for our Functors as well):

irb :001 > Maybe.new(3).bind { |n| Maybe.new(n + 3) }.bind { |n| Maybe.new(n * 2) }
 => Just 12
irb :002 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }
 => Just 12

All right! We’ve leveled up in our ability to encapsulate values and use them how we please. But we’ve moved outside of Functor-land, and we need a new name for this structure we’ve created. If you have followed along so far, then congratulations: you understand the essence of the Monad. This new structure is going to improve our ability to write systems which can be easily composed.

Let’s Talk About Laziness

We’ve made some excellent progress building out a monadic system, but we’re missing one feature Ruby gives us with arrays which makes them extremely useful: the ability to be lazily evaluated. Lazy evaluation gives us a lot of flexibility when it comes to building systems which deal with more data than can fit inside the local system’s memory, infinite sequences, or are just too computationally expensive for eager evaluation to be a desirable trait.

The difference between eager and lazy evaluation can be tough to internalize without a good example, so let’s show off the difference between the two:

eager.rb
#!/usr/bin/env ruby

[1, 2, 3, 4, 5]
  .map { |n| n**2 }
  .map { |n| p n }
  .map { |n| "Stringified: #{n}" }
  .map { |n| p n }
  .take(2)
  .to_a

The only thing which might be surprising here is mapping the p function over the collection a couple of times. This is just a trick to let us view intermediate values. The p method is part of Ruby’s standard library and can be thought of roughly as follows:

def p(obj)
  puts obj.inspect
  obj
end

There’s a bit more to the p method, but this is a sufficient understanding to be able to use p effectively. A similar effect can be achieved by using tap instead of map, but leaning on map allows us to use otherwise identical code between our eager and lazy examples.

So our expectations for what the execution of this program will look like are probably fairly obvious: first we’ll see each of the numbers squared, and then we’ll see the squares prefixed by 'Stringified: '.

[wuest@kaitain]$ ./eager.rb
1
4
9
16
25
"Stringified: 1"
"Stringified: 4"
"Stringified: 9"
"Stringified: 16"
"Stringified: 25"

No surprises here. But let’s tweak our example to use Ruby’s lazy evaluation:

lazy.rb
#!/usr/bin/env ruby

[1, 2, 3, 4, 5]
  .lazy # Let ruby know that this should be evaluated lazily
  .map { |n| n**2 }
  .map { |n| p n }
  .map { |n| "Stringified: #{n}" }
  .map { |n| p n }
  .take(2)
  .to_a

The only difference between this and the eager version is the introduction of a call to lazy. Now, let’s predict what the execution of this file will produce. The most common prediction I’ve heard is that the output will be identical: the evaluation of our calls to map were delayed, but only until we called to_a which forces our lazy data structure to stop being so darn lazy. When we look at the program’s output, we see a different story, though:

[wuest@kaitain]$ ./lazy.rb
1
"Stringified: 1"
4
"Stringified: 4"

This might come as a surprise! We’ve got interleaved results here, which reveals a really cool property: our data structure is traversed only once, only as far as is needed, and our computations are all performed on each element in turn. This is the heart of laziness, which allows us to avoid unnecessary computations and work on datasets much larger than we can hope to hold in memory.

This sounds pretty useful, and it’s worth looking into. Let’s revisit our implementation of the Maybe monad and see how tough it would be to make it so we don’t do any work when we call bind. This is a tough step, so don’t let it discourage you if you take a while to figure out a working solution. For now, our primary concerns are avoiding doing work until it’s needed, and a way to force the evaluation. Lazy enumerables have a #force method which will resolve all pending computations on a lazy structure, so let’s use that name. Here’s one possible solution:

maybe_lazy.rb
# Encapsulate a value which might be nil, but upon which we want to
# conditionally perform computations in a lazy manner
class Maybe
  def initialize(val)
    @_val = val
    @_computations = []
  end

  # Stage a call to fmap sometime in the future
  def fmap(&block)
    next_computation = ->(obj) { obj._fmap(&block) }

    self.class
        .new(@_val)
        .pending(@_computations + [next_computation])
  end

  # Stage a call to bind sometime in the future
  def bind(&block)
    next_computation = ->(obj) { obj._bind(&block) }

    self.class
        .new(@_val)
        .pending(@_computations + [next_computation])
  end

  # Force the lazy structure to complete evaluation
  def force
    @_computations.reduce(self) { |a, e| e.call(a) }
  end

  # Convenience function for working with Maybe in irb/pry
  def inspect
    return 'Nothing' if @_val.nil?
    'Just ' + @_val.inspect
  end

  # Methods which should only be invoked by objects of the same type
  protected

  # Set any computations which are pending to be run.
  def pending(blocks)
    @_computations = blocks
    self
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result wrapped in a shiny new context
  def _fmap
    return self if @_val.nil?
    self.class.new(yield @_val)
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result.  The block passed to bind is expected to wrap the result in a new
  # context for us
  def _bind
    return self if @_val.nil?
    yield @_val
  end
end

There’s plenty that could be improved here, but we seem to have solved the problem at hand. We now avoid doing work until we know it’s necessary, and the other properties of our Maybe monad appear to have been preserved! There’s one minor problem that we’ve created for ourselves, though: forcing doesn’t work the same, depending on our nesting:

irb :001 > Maybe.new(3).bind { |n| Maybe.new(n + 3) }.bind { |n| Maybe.new(n * 2) }.force
 => Just 12
irb :002 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }.force
 => Just 6
irb :003 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }.force.force
 => Just 12

We’ve broken the ground rules we set for ourselves when working with monads, which simply won’t do. There are several solutions to the problem at hand, so don’t stress too hard about finding the “right” one. Just search for a solution which works.

maybe_monad.rb
# Encapsulate a value which might be nil, but upon which we want to
# conditionally perform computations in a lazy manner, while fulfilling the
# rules we agreed upon for monads
class Maybe
  def initialize(val)
    @_val = val
    @_computations = []
    @_forced_value = nil
  end

  # Stage a call to fmap sometime in the future
  def fmap(&block)
    next_computation = ->(obj) { obj._fmap(&block) }

    self.class
        .new(@_val)
        .pending(@_computations + [next_computation])
  end

  # Stage a call to bind sometime in the future
  def bind(&block)
    next_computation = ->(obj) { obj._bind(&block) }

    self.class
        .new(@_val)
        .pending(@_computations + [next_computation])
  end

  # Force the lazy structure to complete evaluation, ensuring nested monads are
  # forced as well
  def force
    # Set @_forced_value the first time we call force, but memoize the result to
    # avoid repeating the work.
    @_forced_value ||= @_computations.reduce(self) { |a, e| e.call(a).force }
  end

  # Convenience function for working with Maybe in irb/pry
  def inspect
    return 'Nothing' if @_val.nil?
    'Just ' + @_val.inspect
  end

  # Methods which should only be invoked by objects of the same type
  protected

  # Set any computations which are pending to be run.
  def pending(blocks)
    @_computations = blocks
    self
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result wrapped in a shiny new context
  def _fmap
    return self if @_val.nil?
    self.class.new(yield @_val)
  end

  # Unwrap the encapsulated value if it isn't nil, perform work, and return the
  # result.  The block passed to bind is expected to wrap the result in a new
  # context for us
  def _bind
    return self if @_val.nil?
    yield @_val
  end
end

Here we once again demonstrate that our work in building these structures is meant to smooth over the nasty details of imperative code. We’ve created an object which checks for the status of whether a given structure has had force called, and saves the result to avoid repeating the work (this is called ‘memoization’). Let’s check that it works, and then move on:

2.3.2 :001 > Maybe.new(3).bind { |n| Maybe.new(n + 3) }.bind { |n| Maybe.new(n * 2) }.force
 => Just 12
2.3.2 :002 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }.force
 => Just 12

Let’s Talk About Purity

Cool, so we’re almost able to circle back to the original question at hand: “what in the world do monads have to do with I/O?!” Let’s talk about what it means to write pure code. A pure function will not produce side effects: whether those side effects are modifying an object in memory (which can lead to potentially difficult to hunt down issues elsewhere in the program) or performing I/O. Let’s look at what can happen when we allow side effects to sneak in:

def last_even(array)
  num = array.pop
  until num.nil? || num.even?
    num = array.pop
  end

  num
end

Though this snippet is fairly short and contrived for the sake of brevity, this class of bug does appear in production codebases. Consider: the programmer who originally produced this code quite likely believes that within the scope of their own method, it’s safe to call pop on this array. And another programmer, based on documentation of the interfaces provided, may well call this method expecting the method not to modify the data structure out from under them. Then a tragicomedy ensues:

irb :001 > nums = [1, 3, 64, 9, 25]
 => [1, 3, 64, 9, 25]
irb :002 > even_num = last_even(nums)
 => 64
irb :003 > highest_num = highest_number(nums)
 => 3

Bugs like this are insidious. They can quietly cause problems in the way that the program runs for months or longer. No crash is caused in many cases, so the bug gets to simply live on, quietly corrupting the data which pases through the system. Let’s update the last_even method so that this bug is fixed, by removing the side effects we incur on the array:

def last_even(array)
  array
    .select { |num| num.even? }
    .last
end

Now when we run our code, the results look far more sensible:

irb :001 > nums = [1, 3, 64, 9, 25]
 => [1, 3, 64, 9, 25]
irb :002 > even_num = last_even(nums)
 => 64
irb :003 > highest_num = highest_number(nums)
 => 64

A few things are immediately obvious at this point. First and foremost, by removing side effects from our code, our ability to hunt down issues in our program’s logic increases. Since we stop having to worry about what might have altered the data we have at any given time behind our backs, we get to look at the computations we perform on our data and regard that as our source of truth. The next thing which is likely to jump out is that pure functions will always return the same value for a given input. No matter how many times last_even([1, 3, 64, 9, 25]) is called, it will always return 64. It seems pretty clear that we could replace this call entirely with even_num = 64 without any impact on the surrounding program. This ability to replace a given function call with its result is called referential transparency.

Let’s talk about I/O

So far we’ve worked with structures which, while useful, tend to be extremely neat and tidy. Our Array and Maybe structures are extremely useful, but in isolation they aren’t going to do much. We’re going to have to perform I/O if we’re going to make our programs do anything useful. I/O is messy, and it ruins our ability to substitute a statement with the result it produces. We can demonstrate this with a couple of examples:

puts 'Ruby is pretty rad!  Say hi!' # => nil

# Let's assume the user types "hello\nworld\n"
x = gets # => "hello\n"
y = gets # => "world\n"

It’s pretty obvious that we can’t replace calls to puts with calls to nil. Our program would waltz its way back from the land of useful programs into a world where it doesn’t actually share the results of the work that’s performed with the user. While this is appealing in that we can never give the user a wrong answer, it’s not the most useful thing in the world.

And then there’s gets. Our very first call to gets results in the string "hello\n" and if we believed that we could substitute all calls to gets with its result, we’d assign the same thing to y, and later on to every other call. This would make it easy to think about our code in terms of bits and pieces of an algorithm which can be safely refactored and replaced however is best for us as programmers, but it would probably cause the users of our software a bit of a headache.

So we clearly cannot get away with simply declaring I/O pure and walking away, our jobs done satisfactorily. We’re going to have to come up with a strategy for managing the effects of our applications. The most common strategy in ruby codebases is to strongly partition off pure and impure portions of the code, such that it is difficult to call impure methods without it being very obvious to the programmer. This is usually accomplished by cordoning impure code off into methods with an exclamation point in their name, denoting something which is effectful (consider the standard library’s distinction between map and map! for a canonical example of this).

Let’s take this a step further, though. Let’s create an entire class to encapsulate our I/O, and see if we can build a clean system out of it. We have a bunch of tools at our disposal based on the previous work we’ve done, and now we can take our first stab at isolating effects! Let’s stick to only out output for the time being:

not_so_pure_output.rb
# Encapsulate the global state so that we can have substitution work while
# allowing for effects to occur
# The class name is an utter lie, presently
class PureOut
  # We probably want something here, but it's not clear what we need yet
  def initialize
  end

  # For now, just wrap puts.  This leaves something to be desired
  def puts(str)
    puts str
    self
  end

  # Convenience function to make objects look nicer in IRB/Pry
  def inspect
    '<Output>'
  end
end

This is kind of lackluster. We now will have an object with which we can chain calls to puts, but that’s about it:

PureOut.new
      .puts('Ruby's pretty rad!')
      .puts('But this could probably be better...')

There’s a bit of a rough edge here: the calls to puts still break substitution. If this seems like something that’s insurmountable, don’t worry: your intuition is correct. We’re going to have to work harder to figure out how to smooth out the turbulent relationship between I/O and our ideal purely functional paradise.

Let’s start with a statement of the obvious: at some point, we will interact with the real world, at which point our ability to substitute code with the result of its evaluation will be broken. Let’s extend this a bit further and admit that puts and gets themselves will also always break substitution. The moment they’re invoked, we cannot reason about our code the same way as before. So what we need is to figure out what the correct interface between our pure code and the gritty real world is going to look like, so that we’re able to represent interactions with the real world, without actually messing with our code’s purity.

One tool we can look at immediately is laziness. With our Maybe monad, we were able to create a lazy context for evaluation whether a value was present or not. Through the use of this lazy structure, we were also able to stage computations to take place without immediately performing them. This sounds a whole lot like what we’re looking for in our new I/O wrapper. Let’s rewrite our PureOut class so that we can make it lazy.

lazy_output.rb
# Encapsulate the global state so that we can have substitution work while
# allowing for effects to occur
# The class name is less of a lie, this time!
class PureOut
  def initialize
    @_stage = []
  end

  # Stage a call to output a string sometime in the future
  def puts(str)
    next_action = ->() { _bind { $stdout.puts str } }

    self.class
        .new
        .pending(@_stage + [next_action])
  end

  # It's go time!  Let's output a bunch of stuff!
  def force
    @_forced_output ||= @_stage.reduce(self) { |_, e| e.call.force }
  end

  # Convenience function to make objects look nicer in IRB/Pry
  def inspect
    '<Output>'
  end

  # Methods which should only be invoked by objects of the same type
  protected

  # Set any computations which are pending to be run.
  def pending(blocks)
    @_stage = blocks
    self
  end

  # Execute the staged actions; since the staged actions are calls to
  # $stdout.puts which always return nil, return self and count on the local
  # puts method to handle managing which context should be returned
  def _bind
    yield
    self
  end
end

And when we run this, we can see that it works as expected!

irb :001 > x = PureOut.new
 => <Output>
irb :002 > y = x.puts('Hello')
 => <Output>
irb :003 > z = y.puts('World!')
 => <Output>
irb :004 > z.force
Hello
World!
 => <Output>

One important detail which is easy to overlook is that a different PureOut object has _bind called on it as each new action is evaluated. In this way we’re able to preserve our ability to replace code with the result of its evaluation by creating a strong dependency on the order of evaluation.

We’ve solved representing output lazily. Next, we need to be able to accept and handle input from the user. Let’s build a PureIn class to perform this. It’s likely to look a lot like PureOut, so don’t worry if it does.

lazy_input.rb
# Encapsulate the global state so that we can have substitution work while
# allowing for effects to occur
# The class name is also 0% lie, more or less!
class PureIn
  def initialize
    @_stage = []
    @_forced_result = nil
  end

  # Stage a request for user input and the action to take based on it
  def gets(&block)
    next_action = ->() { block.call($stdin.gets) }

    self.class
        .new
        .pending(@_stage + [next_action])
  end

  # Accept input from users and take actions
  def force
    @_forced_result ||= @_stage.reduce(self) { |m, e| m._bind(&e).force }
  end

  # Convenience function to make objects look nicer in IRB/Pry
  def inspect
    '<Input>'
  end

  # Methods which should only be invoked by objects of the same type
  protected

  # Set any computations which are pending to be run.
  def pending(blocks)
    @_stage = blocks
    self
  end

  # Execute the staged actions
  def _bind
    yield
  end
end

We can verify that this works the way we hope it does:

irb :001 > x = PureOut.new
 => <Output>
irb :002 > y = PureIn.new
 => <Input>
irb :003 > y.gets { |i| x.puts("Hello, #{i}") }.force
World
Hello, World
 => <Input>


irb :001 > x = highest_number([3, 10, 1, 9, 25, 17])
 => Just 25
irb :002 > y = x.bind { |num| clean_square_root(num) }
 => Just 25
irb :003 > z = y.bind { |num| PureOut.new.puts("The answer is #{num}") }
 => Just 25
irb :004 > z.force
The answer is 5
 => <Output>

Let’s Talk About Unification

So far we’ve got lazy input and output working, but there’s a bit of a wrinkle left. First off, we created a couple of separate structures for input and output, but there’s a lot of shared code here. This sets off alarms indicating that we’re able to refactor some of it away and make a unified structure. What’s more, we achieved laziness, and we could argue that we’ve managed a monadic structure to some degree, but we didn’t implement bind or fmap methods, which we would really prefer be there for our monads.

Fixing this will be our final task; take some time to work through unifying the input and output structures you’ve built. Consider changing puts and gets so that they’re class methods, and implementing them in terms of monadic operation. The Maybe monad is likely to provide inspiration, here.

iomonad.rb
# Provide encapsulation of staged actions which will break substitution upon
# evaluation.
class IOMonad
  def initialize(val = nil)
    @_val = val
    @_stage = []
    @_forced_result = nil
  end

  # Stage a call to output a string sometime in the future
  def self.puts(str)
    new.bind { |_| new($stdout.puts(str)) }
  end

  # Stage a request for user input sometime in the future
  def self.gets
    new.bind { |_| new($stdin.gets) }
  end

  # Stage an action to be performed sometime in the future
  def bind(&block)
    next_action = ->(obj) { obj._bind(&block) }

    self.class
        .new(@_val)
        .pending(@_stage + [next_action])
  end

  # Stage a transformation of encapsulated data sometime in the future
  def fmap(&block)
    next_action = ->(obj) { obj._fmap(&block) }

    self.class
        .new(@_val)
        .pending(@_stage + [next_action])
  end

  # Perform staged actions
  def force
    @_forced_value ||= @_stage.reduce(self) { |a, e| e.call(a).force }
  end

  # Convenience function to make objects look nicer in IRB/Pry
  def inspect
    '<IO>'
  end

  # Methods which should only be invoked by objects of the same type
  protected

  # Set any computations which are pending to be run.
  def pending(blocks)
    @_stage = blocks
    self
  end

  # Unwrap the encapsulated value, perform work, and return a new IO monad
  def _fmap
    self.class.new(yield @_val)
  end

  # Unwrap the encapsulated value, perform work, and return a new IO monad
  def _bind
    yield @_val
  end
end

Most of this code should look familiar by now. We’re able to implement IOMonad.puts and IOMonad.gets in terms of calls to bind, which lets us stage the actions we’re describing without having to evaluate them until the time is right. We should now have a fully monadic I/O system, with far fewer rough edges than our previous two-part system:

irb :001 > x = IOMonad.gets
 => <IO>
irb :002 > y = x.fmap { |str| str.upcase }
 => <IO>
irb :003 > z = y.bind { |str| IOMonad.puts(str) }
 => <IO>
irb :004 > z.force
Hello, monads!
HELLO, MONADS!
 => <IO>

Let’s Talk About the Big Picture

Wow, that was a lot. If you’ve followed along so far, then congratulations on building out a lazy I/O system from scratch! There’s plenty left to consider consider improving, and there’s certainly no reason to stop here; if you feel inspired to keep digging in, you should!

To circle back to the original topic of Haskell’s IO Monad: the implementation we came up with isn’t TOO far off. Just like we got to decide at what point our lazy IO got evaluated and broke substitution, Haskell does the same - from Haskell’s perspective, invoking a program’s main function is the point at which evaluation of IO is forced. This would be no different from if we refused to allow our force method to be called except once inside whatever piece of code we decided was our main jumping off point into the real world.

While the odds of a homegrown monadic IO system in ruby being particularly useful are quite slim, don’t let this come across as just pertaining to Haskell. One of the best uses I’ve found for Haskell is in opening my eyes to ways that I work in other languages. Now that you have another arrow in your quiver, it’s worth looking at your problems through this lens: sometimes thinking in an explicitly monadic manner can simplify one’s workflow considerably, giving us a new way of approaching the design of composable systems.

Each topic covered here could easily fill a book on its own, but hopefully this broad overview helped to give you a better idea of what in the world monads are, why they are used to smooth out I/O in Haskell, and how you might use them in your own projects. If type systems and their impact on the way we program computers is of particular interest, I can’t recommend Types and Programming Languages strongly enough. It covers everything you see here and more in far greater depth.

No matter what else you take away from this, I hope these points stick with you:

  • The jargon which is frequently encountered in Haskell literature can be useful for conveying ideas, so it’s worth knowing:
    • If something is a Functor, that means it just adheres to a certain set of rules we defined; there’s nothing special about it beyond that.
    • If something is a Monad, that also only means it adheres to a set of rules we defined! Remember, our Maybe monad just smooths over the details of checking for nil; there’s no magic here.
  • Pure code makes our lives easier when it comes time to figure out exactly what a given piece of code does, but we will eventually have to cope with talking to the real world.
  • As programmers, we get to define the boundary between our code and the real world.

Update 2017.06.14: Thanks to the amazing work of Y Torii, this post has been translated into Japanese!