Functional Programming with Ruby
Bài đăng này đã không được cập nhật trong 7 năm
Continue with functional programming: Monads
In this post I will try to get a grasp at Monads with Ruby.
Monads is a very important concept in functional programming.
So what is monad and what is it for?
Monads can be thought of as composable computation descriptions. The essence of monad is thus separation of composition timeline from the composed computation's execution timeline, as well as the ability of computation to implicitly carry extra data, as pertaining to the computation itself, in addition to its one (hence the name) output, that it will produce when run (or queried, or called upon). This lends monads to supplementing pure calculations with features like I/O, common environment or state, etc
What is it for?
- Monads are used for getting imperative behavior out of functional programs
- Operations with side effect like IO, error handling and failure at runtime, changing state
- Non-deterministic operations, i.e. operations that can have different results given the same input
- Essentially, Monads allow the computation themselves to be isolated from the side effects and non determinism, thus remaining purely functional.
See monad
A monad, for our purposes, is made up of three things:
A container type.
- the operation wrap: puts a single value in an instance of the container; Haskell calls this return
- the operation pass: extracts the values from the container
- filters them through a function (we’ll use a block); Haskell calls this “bind” (spelt >>=) These three ingredients make up a monad. Most monads have some additional operations available, but those vary from monad to monad.
What good are monads? They let you chain pass operations together to make little computational pipelines, with rules of your choosing. They don’t manipulate values themselves — that’s the job of the blocks (functions) you plumb together using the monad.
What do we know about monads?
They can be made from pretty much any sort of generic container. You can wrap a single value in a monad (wrap) You can extract values from a monad and give them individually to a function to process and repackage (pass)
class Identity
def initialize( value )
@value = value
end
end
wrap
First thing, make a way to wrap a value in our monad. Identity::new puts a value in an instance of Identity, right?
We’ll use that to make Identity::wrap.
def Identity::wrap(value)
new(value)
end
pass
Second thing, we’ve got to make a method that extracts the contents and gives them to a function. This being Ruby, that may as well mean a block.
class Identity def pass yield @value end end
There are also three laws that wrap and pass have to follow to make Identity a real monad.
- Calling pass on a newly-wrapped value should have the same effect as giving that value directly to the block.
- pass with a block that simply calls wrap on its value should produce the exact same values, wrapped up again.
- nesting pass blocks should be equivalent to calling them sequentially
Let’s look at each of these in detail. For our purposes, we’ll assume that f and g are two functions which take a value, do something with it, and return an already-wrapped result.
The First Law: wrap as a left-identity for pass
This just means that:
Identity::wrap( blah ).pass do |value|
f( value )
end
Should have exactly the same effect as:
f( blah ) The Second Law: wrap as a right-identity for pass
This just means that:
someidentity.pass do |value|
Identity::wrap( value )
end
Should produce an instance of Identity with the same contents as the original (someidentity). In other words: can we trust pass and wrap not to change the values on their own?
The Third Law
The third law just means that chaining pass blocks should produce the same effect as nesting them. To wit:
someidentity.pass do |value_a|
f( value_a )
end.pass |value_b|
g( value_b )
end
and
someidentity.pass do |value_a|
f( value_a ).pass do |value_b|
g( value_b )
end
end
Now that Identity is a really, truly monad, let’s grant it one last thing before it flutters away. Most monads do give you some way to get a value out directly, the details of which vary from monad to monad since they all work differently.
For Identity, though, it can be a simple unwrapping.
class Identity attr_reader :value end
Implementation of Monads
Here is another example I get from another site, But I want to make simpler. This give quite a good insight to what monad is:
Imagine that we have a project management application with different kinds of models:
Book = Struct.new(:author)
Author = Struct.new(:nationality)
Nationality = Struct.new(:language)
Suppose we have a method
def get_language(book)
book.author.nationality.language
end
Here is the example how we might get language of the book:
2.3.1 :001 > Book = Struct.new(:author)
=> Book
2.3.1 :002 > Author = Struct.new(:nationality)
=> Author
2.3.1 :003 > Nationality = Struct.new(:language)
=> Nationality
2.3.1 :004 > def get_language(book)
2.3.1 :005?> book.author.nationality.language
2.3.1 :006?> end
=> :get_language
2.3.1 :007 > nationality = Nationality.new('English')
=> #<struct Nationality language="English">
2.3.1 :009 > author = Author.new(nationality)
=> #<struct Author nationality=#<struct Nationality language="English">>
2.3.1 :010 > book = Book.new(author)
=> #<struct Book author=#<struct Author nationality=#<struct Nationality language="English">>>
2.3.1 :011 > get_language(book)
=> "English"
But situtation is not always good, We may get into nil
2.3.1 :013 > book = Book.new(Author.new(nil))
=> #<struct Book author=#<struct Author nationality=nil>>
2.3.1 :014 > get_language(book)
NoMethodError: undefined method `language' for nil:NilClass
To prevent this we write something like this
def get_language(book)
unless book.nil?
author = book.author
end
unless author.nil?
nationality = author.nationality
end
unless nationality.nil?
language = nationality.language
end
end
We create an Optional class that act as a decorate class which implment try methods.
we can write the method like this:
def get_language(book)
optional_book = Optional.new(book)
optional_author = optional_book.try { |b| Optional.new(b.author) }
optional_nationality = optional_author.try { |a| Optional.new(a.nationality) }
optional_language = optional_nationality.try { |n| Optional.new(n.language) }
language = optional_language.value
end
class Optional
def try(&block)
if value.nil?
Optional.new(nil)
else
block.call(value)
end
end
end
book = Book.new(Author.new(nil))
get_language(book)
=> nil
But we can do even better by trying to remove try completely by using method method_missing instead and change method try to and_then.
Optional = Struct.new(:value) do
def and_then(&block)
if value.nil?
Optional.new(nil)
else
block.call(value)
end
end
def method_missing(*args, &block)
and_then do |value|
Optional.new(value.public_send(*args, &block))
end
end
end
def get_language(project)
book.author.nationality.language
end
And then we wouldn’t have to remember to check for nil at all! We could write the method the way we did in the first place and it would just work.
Let's think of monads as an abtraction that connects functions together so that types are not leaked out of the expected domain.
The big benefit of monads is that they give us a common interface which allows us to do one thing: connect together a sequence of operations. The #and_then method means “do the next thing”, but the way it “does the next thing” depends on which monad you use:
Optional#and_then does the next thing only if the value isn’t nil
All rights reserved