Monads

Monad

In functional programming, a monad is a structure that represents computations defined as sequences of steps: a type with a monad structure defines what it means to chain operations, or nest functions of that type together. This allows the programmer to build pipelines that process data in steps, in which each action is decorated with additional processing rules provided by the monad

src: Wikipedia

Formally, a monad consists of a type constructor M and two operations, bind and return.

The Writer monad allows a process to carry additional information "on the side", along with the computed value. This can be useful to log error or debugging information which is not the primary result.

Simple Example: Writer Monad

Simple Example: Writer Monad

var unit = function(value) { return [value, ''] };
var bind = function(monadicValue, transformWithLog) {
    var value  = monadicValue[0],
        log = monadicValue[1],
        result = transformWithLog(value);
    return [ result[0], log + result[1] ];
};
var pipeline = function(monadicValue, functions) {
    for (var key in functions) {
        monadicValue = bind(monadicValue, functions[key]);
    }
    return monadicValue;
};
var squared = function(x) {
    return [x * x, 'was squared.'];
};
 
var halved = function(x) {
    return [x / 2, 'was halved.'];
};
pipeline(unit(4), [squared, halved]); // [8, "was squared.was halved."]

Monads in ruby

Inspired by @tomstuart codon.com

Optional

Nil Problem

Project = Struct.new(:creator)
Person  = Struct.new(:address)
Address = Struct.new(:country)
Country = Struct.new(:capital)
City    = Struct.new(:weather)
def weather_for(project)
  project.creator.address.country.capital.weather
end
def weather_for(project)
  unless project.nil?
    creator = project.creator
    unless creator.nil?
      address = creator.address
      unless address.nil?
        country = address.country
        unless country.nil?
          capital = country.capital
          unless capital.nil?
            weather = capital.weather
          end
        end
      end
    end
  end
end
def weather_for(project)
  project.
    try(:creator).
    try(:address).
    try(:country).
    try(:capital).
    try(:weather)
end

Let's decorate!

class Optional
  def try(*args, &block)
    if value.nil?
      nil
    else
      value.public_send(*args, &block)
    end
  end
end
def weather_for(project)
  optional_project = Optional.new(project)
  optional_creator = Optional.new(optional_project.try(:creator))
  optional_address = Optional.new(optional_creator.try(:address))
  optional_country = Optional.new(optional_address.try(:country))
  optional_capital = Optional.new(optional_country.try(:capital))
  optional_weather = Optional.new(optional_capital.try(:weather))
  weather          = optional_weather.value
end

Block to the rescue!

class Optional
  def try(&block)
    if value.nil?
      nil
    else
      block.call(value)
    end
  end
end
def weather_for(project)
  optional_project = Optional.new(project)
  optional_creator = optional_project.try { |project| Optional.new(project.creator) }
  optional_address = optional_creator.try { |creator| Optional.new(creator.address) }
  optional_country = optional_address.try { |address| Optional.new(address.country) }
  optional_capital = optional_country.try { |country| Optional.new(country.capital) }
  optional_weather = optional_capital.try { |capital| Optional.new(capital.weather) }
  weather          = optional_weather.value
end

Nil is not chainable anymore :(

class Optional
  def try(&block)
    if value.nil?
      Optional.new(nil)
    else
      block.call(value)
    end
  end
end

Rename #try to #and_then

class Optional
  def and_then(&block)
    if value.nil?
      Optional.new(nil)
    else
      block.call(value)
    end
  end
end
def weather_for(project)
  optional_project = Optional.new(project)
  optional_creator = optional_project.and_then { |project| Optional.new(project.creator) }
  optional_address = optional_creator.and_then { |creator| Optional.new(creator.address) }
  optional_country = optional_address.and_then { |address| Optional.new(address.country) }
  optional_capital = optional_country.and_then { |country| Optional.new(country.capital) }
  optional_weather = optional_capital.and_then { |capital| Optional.new(capital.weather) }
  weather          = optional_weather.value
end
def weather_for(project)
  Optional.new(project).
    and_then { |project| Optional.new(project.creator) }.
    and_then { |creator| Optional.new(creator.address) }.
    and_then { |address| Optional.new(address.country) }.
    and_then { |country| Optional.new(country.capital) }.
    and_then { |capital| Optional.new(capital.weather) }.
    value
end

Let's make ruby work more

class Optional
  def method_missing(*args, &block)
    and_then do |value|
      Optional.new(value.public_send(*args, &block))
    end
  end
end

And our method looks great:

def weather_for(project)
  Optional.new(project).
    creator.address.country.capital.weather.value
end

Many

Problem

Blog     = Struct.new(:categories)
Category = Struct.new(:posts)
Post     = Struct.new(:comments)
def words_in(blogs)
  blogs.flat_map { |blog|
    blog.categories.flat_map { |category|
      category.posts.flat_map { |post|
        post.comments.flat_map { |comment|
          comment.split(/\s+/)
        }
      }
    }
  }
end

Example data

blogs = [
  Blog.new([
    Category.new([
      Post.new(['I love cats', 'I love dogs']),
      Post.new(['I love mice', 'I love pigs'])
    ]),
    Category.new([
      Post.new(['I hate cats', 'I hate dogs']),
      Post.new(['I hate mice', 'I hate pigs'])
    ])
  ]),
  Blog.new([
    Category.new([
      Post.new(['Red is better than blue'])
    ]),
    Category.new([
      Post.new(['Blue is better than red'])
    ])
  ])
]
>> words_in(blogs)
=> ["I", "love", "cats", "I", "love", "dogs", "I",
    "love", "mice", "I", "love", "pigs", "I",
    "hate", "cats", "I", "hate", "dogs", "I",
    "hate", "mice", "I", "hate", "pigs", "Red",
    "is", "better", "than", "blue", "Blue", "is",
    "better", "than", "red"]

Let's use a Monad again!!!11one

Many = Struct.new(:values) do
  def and_then(&block)
    Many.new(values.map(&block).flat_map(&:values))
  end
end
def words_in(blogs)
  Many.new(blogs).and_then do |blog|
    Many.new(blog.categories).and_then do |category|
      Many.new(category.posts).and_then do |post|
        Many.new(post.comments).and_then do |comment|
          Many.new(comment.split(/\s+/))
        end
      end
    end
  end.values
end
def words_in(blogs)
  Many.new(blogs).and_then do |blog|
    Many.new(blog.categories)
  end.and_then do |category|
    Many.new(category.posts)
  end.and_then do |post|
    Many.new(post.comments)
  end.and_then do |comment|
    Many.new(comment.split(/\s+/))
  end.values
end
def words_in(blogs)
  Many.new(blogs).
    and_then { |blog    | Many.new(blog.categories)      }.
    and_then { |category| Many.new(category.posts)       }.
    and_then { |post    | Many.new(post.comments)        }.
    and_then { |comment | Many.new(comment.split(/\s+/)) }.
    values
end

More sugar

class Many
  def method_missing(*args, &block)
    and_then do |value|
      Many.new(value.public_send(*args, &block))
    end
  end
end
def words_in(blogs)
  Many.new(blogs).
    categories.posts.comments.split(/\s+/).
    values
end

Eventually

Problem - API data

require 'uri_template'

get_json('https://api.github.com/') do |urls|
  org_url_template = URITemplate.new(urls['organization_url'])
  org_url = org_url_template.expand(org: 'ruby')

  get_json(org_url) do |org|
    repos_url = org['repos_url']

    get_json(repos_url) do |repos|
      most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
      repo_url = most_popular_repo['url']

      get_json(repo_url) do |repo|
        contributors_url = repo['contributors_url']

        get_json(contributors_url) do |users|
          most_prolific_user = users.max_by { |user| user['contributions'] }
          user_url = most_prolific_user['url']

          get_json(user_url) do |user|
            puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
          end
        end
      end
    end
  end
end

One Monad to room 208 please!

Eventually = Struct.new(:block) do
  def initialize(&block)
    super(block)
  end

  def run(&success)
    block.call(success)
  end

  def and_then(&block)
    Eventually.new do |success|
      run do |value|
        block.call(value).run(&success)
      end
    end
  end
end

Wrap #get_json

Eventually.new { |s| get_json('https://api.github.com/', &s) }.and_then do |urls|
  org_url_template = URITemplate.new(urls['organization_url'])
  org_url = org_url_template.expand(org: 'ruby')

  Eventually.new { |s| get_json(org_url, &s) }.and_then do |org|
    repos_url = org['repos_url']

    Eventually.new { |s| get_json(repos_url, &s) }.and_then do |repos|
      most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
      repo_url = most_popular_repo['url']

      Eventually.new { |s| get_json(repo_url, &s) }.and_then do |repo|
        contributors_url = repo['contributors_url']

        Eventually.new { |s| get_json(contributors_url, &s) }.and_then do |users|
          most_prolific_user = users.max_by { |user| user['contributions'] }
          user_url = most_prolific_user['url']

          Eventually.new { |s| get_json(user_url, &s) }
        end
      end
    end
  end
end.run do |user|
  puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
end

One method - one job

def get_github_api_urls
  github_root_url = 'https://api.github.com/'

  Eventually.new { |success| get_json(github_root_url, &success) }
end
def get_org(urls, name)
  org_url_template = URITemplate.new(urls['organization_url'])
  org_url = org_url_template.expand(org: name)

  Eventually.new { |success| get_json(org_url, &success) }
end
def get_repos(org)
  repos_url = org['repos_url']

  Eventually.new { |success| get_json(repos_url, &success) }
end

And so on...

Final API call

get_github_api_urls.and_then do |urls|
  get_org(urls, 'ruby')
end.and_then do |org|
  get_repos(org)
end.and_then do |repos|
  get_most_popular_repo(repos)
end.and_then do |repo|
  get_contributors(repo)
end.and_then do |users|
  get_most_prolific_user(users)
end.run do |user|
  puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
end
get_github_api_urls.
  and_then { |urls | get_org(urls, 'ruby')         }.
  and_then { |org  | get_repos(org)                }.
  and_then { |repos| get_most_popular_repo(repos)  }.
  and_then { |repo | get_contributors(repo)        }.
  and_then { |users| get_most_prolific_user(users) }.
  run do |user|
    puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
  end

Thank You

Monads

By Bartek Kruszczyński