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 Fixnum
s 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:
-> [b] [a]
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.”
-> b) -> [a] -> [b] (a
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 mappingg(x)
followed by mappingf(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)
= array.sort { |x, y| y <=> x }
sorted .first
sortedend
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)
= array.sort { |x, y| y <=> x }
sorted Maybe.new(sorted.first)
end
Nifty. We can test it out now:
001 > highest_number([1, 2, 3]).fmap { |n| n**2 }.fmap { |n| n.to_s }
irb :=> Just "9"
002 > highest_number([]).fmap { |n| n**2 }.fmap { |n| n.to_s }
irb :=> 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)
= Math.sqrt(number)
square = square.to_i if square == square.to_i
clean 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?
001 > highest_number([1, 4, 25, 9, 2]).fmap { |n| clean_square_root(n) }
irb :=> Just Just 5
002 > highest_number([1, 4, 26, 9, 2]).fmap { |n| clean_square_root(n) }
irb :=> Just Nothing
003 > highest_number([]).fmap { |n| clean_square_root(n) }
irb :=> 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:
.fmap do |l1|
five_layer_burrito.fmap do |l2|
l1.fmap do |l3|
l2.fmap do |l4|
l3.fmap { |l5| l5 + "I have five mouths and I must scream" }
l4end
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:
001 > clean_square_root(25).bind { |n| Maybe.new(n) } # This is called the "right" identity
irb :=> Just 5
002 > Maybe.new(25).bind { |n| clean_square_root(n) } # This is called the "left" identity
irb :=> 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):
001 > Maybe.new(3).bind { |n| Maybe.new(n + 3) }.bind { |n| Maybe.new(n * 2) }
irb :=> Just 12
002 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }
irb :=> 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
objend
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)
= ->(obj) { obj._fmap(&block) }
next_computation
self.class
.new(@_val)
.pending(@_computations + [next_computation])
end
# Stage a call to bind sometime in the future
def bind(&block)
= ->(obj) { obj._bind(&block) }
next_computation
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:
001 > Maybe.new(3).bind { |n| Maybe.new(n + 3) }.bind { |n| Maybe.new(n * 2) }.force
irb :=> Just 12
002 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }.force
irb :=> Just 6
003 > Maybe.new(3).bind { |n| Maybe.new(n + 3).bind { |n| Maybe.new(n * 2) } }.force.force
irb :=> 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)
= ->(obj) { obj._fmap(&block) }
next_computation
self.class
.new(@_val)
.pending(@_computations + [next_computation])
end
# Stage a call to bind sometime in the future
def bind(&block)
= ->(obj) { obj._bind(&block) }
next_computation
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)
= array.pop
num until num.nil? || num.even?
= array.pop
num end
numend
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:
001 > nums = [1, 3, 64, 9, 25]
irb :=> [1, 3, 64, 9, 25]
002 > even_num = last_even(nums)
irb :=> 64
003 > highest_num = highest_number(nums)
irb :=> 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:
001 > nums = [1, 3, 64, 9, 25]
irb :=> [1, 3, 64, 9, 25]
002 > even_num = last_even(nums)
irb :=> 64
003 > highest_num = highest_number(nums)
irb :=> 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"
= gets # => "hello\n"
x = gets # => "world\n" y
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)
= ->() { _bind { $stdout.puts str } }
next_action
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!
001 > x = PureOut.new
irb :=> <Output>
002 > y = x.puts('Hello')
irb :=> <Output>
003 > z = y.puts('World!')
irb :=> <Output>
004 > z.force
irb :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)
= ->() { block.call($stdin.gets) }
next_action
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:
001 > x = PureOut.new
irb :=> <Output>
002 > y = PureIn.new
irb :=> <Input>
003 > y.gets { |i| x.puts("Hello, #{i}") }.force
irb :World
Hello, World
=> <Input>
001 > x = highest_number([3, 10, 1, 9, 25, 17])
irb :=> Just 25
002 > y = x.bind { |num| clean_square_root(num) }
irb :=> Just 25
003 > z = y.bind { |num| PureOut.new.puts("The answer is #{num}") }
irb :=> Just 25
004 > z.force
irb :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)
.bind { |_| new($stdout.puts(str)) }
newend
# Stage a request for user input sometime in the future
def self.gets
.bind { |_| new($stdin.gets) }
newend
# Stage an action to be performed sometime in the future
def bind(&block)
= ->(obj) { obj._bind(&block) }
next_action
self.class
.new(@_val)
.pending(@_stage + [next_action])
end
# Stage a transformation of encapsulated data sometime in the future
def fmap(&block)
= ->(obj) { obj._fmap(&block) }
next_action
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:
001 > x = IOMonad.gets
irb :=> <IO>
002 > y = x.fmap { |str| str.upcase }
irb :=> <IO>
003 > z = y.bind { |str| IOMonad.puts(str) }
irb :=> <IO>
004 > z.force
irb :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 fornil
; 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!