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
Monads
- 1,158