Monads allow you to chain together operations in your program without any unintended consequences. And they are a functional design pattern that are widely used and some languages even force you into using them like Haskell. I hope to build upon the definition of a monad as the post goes on.
An analogy from the wikipedia, states they are something like an assembly line where the conveyor belts transports data between functional units and they are transformed one step at a time. During this process, the conveyor belt does not stop.
I like to think of them as a type with a wrapped value inside it. For example, a list can have data inside of it and that can be thought of as the value or it can be an empty list with no data, but the type is still a list. Some languages, like Java 8, have the built in Optional type. There is data inside the optional like an integer or string or it could be empty with no data inside it. And the type is still an optional. Both Lists and Optionals are Monads.
Monads have a bind function that allows them to continue being transported down the assembly line. The bind function must abide by this definition
ma X (a -> mb) -> mb
Where ma
is a wrapped value and a
defines the type of the value inside the wrapped object. For example, the list [1, 2, 3]
m
is a list (the type of the wrapper) and a
is an integer (the type of the data inside the wrapped object)
So the bind function takes a wrapped object of type a
and a function and converts it to a wrapped object of type b
.
I want to slowly start introducing an example of Monads that Emmanuel and I started writing in Ruby. We wrote an IO example, the purpose of this small program is to be able to go through the IO process without having the program crash. We first defined an object that is like a wrapper to our data.
class MIO
attr_reader :value
def initialize(value)
@value = value
end
def empty?
value.nil?
end
class << self
def new_empty
self.new(nil)
end
end
end
This way we wrap our data in the MIO
object. For example, monad_io = MIO.new(5)
and to declare an empty one monad_io = MIO.new_empty
. So if we call monad_io.empty?
we get a boolean back as to if there is data in the MIO object or not.
Next, we define functions that perform IO operations. They are defined as lambdas and can be invoked with .call()
. For example, get_line.call()
class MIO
attr_reader :value
def initialize(value)
@value = value
end
def empty?
value.nil?
end
class << self
def new_empty
self.new(nil)
end
def get_line
@@get_line
end
def get_contents
@@get_contents
end
def puts_str
@@puts_str
end
@@get_line = lambda {
user_input = gets.chomp
MIO.new(user_input) }
@@get_contents = lambda { |filename|
begin
file = File.open(filename) do |f1|
contents = IO.read(filename)
contents.nil? ? MIO.new_empty : MIO.new(contents)
end
rescue Exception
MIO.new_empty
end
}
@@puts_str = lambda { |contents|
puts contents
MIO.new_empty
}
end
end
Since they are lambdas the function signature is not so clear. But they take a value (not the wrapped value) and perform some sort of IO operation on them. They return an empty MIO
object if the operation is a failure and a MIO
object with a value if the operation was sucessuful. Now all we need is a bind that glues everything together - it takes a wrapped object and a function and returns a new wrapped object.
class MIO
attr_reader :value
def initialize(value)
@value = value
end
def empty?
value.nil?
end
def bind(function)
if self.empty?
MIO.new_empty
else
function.call(self.value)
end
end
class << self
def get_line
@@get_line
end
def get_contents
@@get_contents
end
def puts_str
@@puts_str
end
def new_empty
self.new(nil)
end
@@get_line = lambda {
user_input = FakeIO.gets
MIO.new(user_input) }
@@get_contents = lambda { |filename|
begin
file = File.open(filename) do |f1|
contents = IO.read(filename)
contents.nil? ? MIO.new_empty : MIO.new(contents)
end
rescue Exception
MIO.new_empty
end
}
@@puts_str = lambda { |contents|
puts contents
MIO.new_empty
}
end
end
In this implementation of bind, it operates on an instance of MIO
so it only needs the function passed in. Now we can do chain together functions like this
monad_io = MIO.get_line.call()
.bind(MIO.get_contents)
.bind(MIO.puts_str)
The big advantage is that our whole program will run no matter what the inputs are for each function if they exist or not. For example, if the user does not enter a valid file name in get_line
the rest of the functions are still able to operate, the program never crashes. Additionally, this pattern allows for consistency in our program. As you can see each IO
function is crafted in a similar manner.