0

Functional Programming with Ruby

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí