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.
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."]
Inspired by @tomstuart codon.com
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
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
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
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
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
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
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"]
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
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
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
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
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
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...
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