$30 off During Our Annual Pro Sale. View Details »

Refactoring Ruby with Monads

Refactoring Ruby with Monads

Monads are in danger of becoming a bit of a joke: for every person who raves about them, there’s another person asking what in the world they are, and a third person writing a confusing tutorial about them. With their technical-sounding name and forbidding reputation, monads can seem like a complex, abstract idea that’s only relevant to mathematicians and Haskell programmers.

Forget all that! In this pragmatic talk we roll up our sleeves and get stuck into refactoring some awkward Ruby code, using the good parts of monads to tackle the problems we encounter along the way. We see how the straightforward design pattern underlying monads can help us to make our code simpler, clearer and more reusable by uncovering its hidden structure, and we all leave with a shared understanding of what monads actually are and why people won’t shut up about them.

Given at eurucamp 2014 (http://2014.eurucamp.org/), BaRuCo 2014 (http://lanyrd.com/2014/baruco/) and GoGaRuCo 2014 (http://lanyrd.com/2014/gogaruco/). The accompanying code is at https://github.com/tomstuart/monads, and an article version is available at https://tomstu.art/refactoring-ruby-with-monads.

Tom Stuart

August 02, 2014
Tweet

More Decks by Tom Stuart

Other Decks in Programming

Transcript

  1. REFACTORING RUBY
    WITH MONADS
    @tomstuart • GoGaRuCo • 2014-09-19

    View Slide

  2. WARM UP

    View Slide

  3. ANALOGIES

    View Slide

  4. WHAT’S A
    STACK?

    View Slide

  5. A KIND OF VALUE…

    View Slide

  6. WITH CERTAIN
    OPERATIONS


    View Slide

  7. #push(value)
    #pop
    #top
    #empty?
    .empty

    View Slide

  8. THAT FOLLOW
    CERTAIN RULES

    View Slide

  9. stack.push(value).top == value
    stack.push(value).pop == stack
    empty.empty? == truee
    stack.push(value).empty? == false

    View Slide

  10. WE CAN
    IMPLEMENT THEM
    HOWEVER WE LIKE

    View Slide

  11. ArrayStack = Struct.new(:values) do
    def push(value)
    ArrayStack.new([value] + values)
    end
    !
    def top
    values.first
    end
    !
    def pop
    ArrayStack.new(values.drop(1))
    end
    !
    def empty?
    values.empty?
    end
    !
    def self.empty
    new([])
    end
    end

    View Slide

  12. LinkedListStack = Struct.new(:top, :pop) do
    def push(value)
    LinkedListStack.new(value, self)
    end
    !
    # def top
    # # we get this for free!
    # end
    !
    # def pop
    # # and this!
    # end
    !
    def empty?
    pop.nil?
    end
    !
    def self.empty
    new(nil, nil)
    end
    end

    View Slide

  13. WE CAN USE THEM
    WITHOUT KNOWING
    THEIR IMPLEMENTATION

    View Slide

  14. >> stack = ArrayStack.empty
    => #
    >> stack.push('hello').push('world').pop.top
    => "hello"
    >> stack = LinkedListStack.empty
    => #
    >> stack.push('hello').push('world').pop.top
    => "hello"

    View Slide

  15. WE CAN
    DEFINE MORE
    OPERATIONS

    View Slide

  16. module Stack
    def size
    if empty?
    0
    else
    pop.size + 1
    end
    end
    end
    !
    ArrayStack.include(Stack)
    LinkedListStack.include(Stack)

    View Slide

  17. >> ArrayStack.empty.size
    => 0
    >> ArrayStack.empty.push('hello').push('world').size
    => 2
    >> LinkedListStack.empty.size
    => 0
    >> LinkedListStack.empty.push('hello').push('world').size
    => 2

    View Slide

  18. OPERATIONS
    &
    RULES

    View Slide

  19. COMMON INTERFACE
    &
    SHARED FUNCTIONALITY

    View Slide

  20. WHAT’S A
    COLLECTION?

    View Slide

  21. A KIND OF VALUE…

    View Slide

  22. WITH CERTAIN
    OPERATIONS


    View Slide

  23. #each

    View Slide

  24. THAT FOLLOW
    CERTAIN RULES

    View Slide

  25. #each
    calls a block with a value
    zero or more times
    in immediate sequence

    View Slide

  26. WE CAN
    IMPLEMENT IT
    HOWEVER WE LIKE

    View Slide

  27. class HardcodedCollection
    def each(&block)
    block.call(1)
    block.call(2)
    block.call(3)
    block.call(4)
    block.call(5)
    end
    end

    View Slide

  28. class GeneratedCollection
    def each(&block)
    number = 1
    while number <= 5
    block.call(number)
    number = number + 1
    end
    end
    end

    View Slide

  29. WE CAN USE IT
    WITHOUT KNOWING
    ITS IMPLEMENTATION

    View Slide

  30. >> collection = HardcodedCollection.new
    => #
    >> collection.each { |n| puts n }
    1
    2
    3
    4
    5
    => nil
    >> collection = GeneratedCollection.new
    => #
    >> collection.each { |n| puts n }
    1
    2
    3
    4
    5
    => nil

    View Slide

  31. WE CAN
    DEFINE MORE
    OPERATIONS

    View Slide

  32. module Collection
    def select(&block)
    results = []
    each do |value|
    results << value if block.call(value)
    end
    results
    end
    end
    !
    HardcodedCollection.include(Collection)
    GeneratedCollection.include(Collection)

    View Slide

  33. >> HardcodedCollection.new.select { |n| n.odd? }
    => [1, 3, 5]
    !
    >> GeneratedCollection.new.select { |n| n.odd? }
    => [1, 3, 5]

    View Slide

  34. OPERATIONS
    &
    RULES

    View Slide

  35. COMMON INTERFACE
    &
    SHARED FUNCTIONALITY

    View Slide

  36. WHAT ARE
    STACKS &
    COLLECTIONS?

    View Slide

  37. ABSTRACT DATA TYPE

    View Slide

  38. (ABSTRACT,
    BUT GOOD!)

    View Slide

  39. LET’S DO SOME
    REFACTORING!

    View Slide

  40. HANDLING NIL

    View Slide

  41. Project = Struct.new(:creator)
    Person = Struct.new(:address)
    Address = Struct.new(:country)
    Country = Struct.new(:capital)
    City = Struct.new(:weather)

    View Slide

  42. def weather_for(project)
    project.creator.address.
    country.capital.weather
    end

    View Slide

  43. >> city = City.new('sunny')
    => #
    !
    >> country = Country.new(city)
    => #
    !
    >> address = Address.new(country)
    => #
    !
    >> person = Person.new(address)
    => #
    !
    >> project = Project.new(person)
    => #

    View Slide

  44. >> weather_for(project)
    => "sunny"

    View Slide

  45. >> bad_project = Project.new(Person.new(Address.new(nil)))
    => #
    !
    >> weather_for(bad_project)
    NoMethodError: undefined method `capital' for nil:NilClass

    View Slide

  46. def weather_for(project)
    project.creator.address.
    country.capital.weather
    end

    View Slide

  47. def weather_for(project)
    creator = project.creator
    address = creator.address
    country = address.country
    capital = country.capital
    weather = capital.weather
    end

    View Slide

  48. 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

    View Slide

  49. def weather_for(project)
    unless project.nil?
    creator = project.creator
    end
    !
    unless creator.nil?
    address = creator.address
    end
    !
    unless address.nil?
    country = address.country
    end
    !
    unless country.nil?
    capital = country.capital
    end
    !
    unless capital.nil?
    weather = capital.weather
    end
    end

    View Slide

  50. # activesupport/lib/active_support/core_ext/object/try.rb
    !
    class Object
    def try(*a, &b)
    if a.empty? && block_given?
    yield self
    else
    public_send(*a, &b) if respond_to?(a.first)
    end
    end
    end
    !
    class NilClass
    def try(*args)
    nil
    end
    end

    View Slide

  51. def weather_for(project)
    unless project.nil?
    creator = project.creator
    end
    !
    unless creator.nil?
    address = creator.address
    end
    !
    unless address.nil?
    country = address.country
    end
    !
    unless country.nil?
    capital = country.capital
    end
    !
    unless capital.nil?
    weather = capital.weather
    end
    end

    View Slide

  52. def weather_for(project)
    creator = project.try(:creator)
    address = creator.try(:address)
    country = address.try(:country)
    capital = country.try(:capital)
    weather = capital.try(:weather)
    end

    View Slide

  53. def weather_for(project)
    project.
    try(:creator).
    try(:address).
    try(:country).
    try(:capital).
    try(:weather)
    end

    View Slide

  54. # activesupport/lib/active_support/core_ext/object/try.rb
    !
    class Object
    def try(*a, &b)
    if a.empty? && block_given?
    yield self
    else
    public_send(*a, &b) if respond_to?(a.first)
    end
    end
    end
    !
    class NilClass
    def try(*args)
    nil
    end
    end

    View Slide

  55. class Object
    remove_method :try
    end
    !
    class NilClass
    remove_method :try
    end

    View Slide

  56. Optional = Struct.new(:value)

    View Slide

  57. >> optional_string = Optional.new('hello')
    => #
    >> optional_string.value
    => "hello"
    >> optional_string = Optional.new(nil)
    => #
    >> optional_string.value
    => nil

    View Slide

  58. class Optional
    def try(*args, &block)
    if value.nil?
    nil
    else
    value.public_send(*args, &block)
    end
    end
    end

    View Slide

  59. >> optional_string = Optional.new('hello')
    => #
    >> length = optional_string.try(:length)
    => 5
    >> optional_string = Optional.new(nil)
    => #
    >> length = optional_string.try(:length)
    => nil

    View Slide

  60. def weather_for(project)
    creator = project.try(:creator)
    address = creator.try(:address)
    country = address.try(:country)
    capital = country.try(:capital)
    weather = capital.try(:weather)
    end

    View Slide

  61. 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

    View Slide

  62. class Optional
    def try(*args, &block)
    if value.nil?
    nil
    else
    value.public_send(*args, &block)
    end
    end
    end

    View Slide

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

    View Slide

  64. 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

    View Slide

  65. 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

    View Slide

  66. >> weather_for(project)
    => "sunny"
    !
    >> weather_for(bad_project)
    NoMethodError: undefined method `capital' for nil:NilClass

    View Slide

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

    View Slide

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

    View Slide

  69. >> weather_for(project)
    => "sunny"
    !
    >> weather_for(bad_project)
    => nil

    View Slide

  70. 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

    View Slide

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

    View Slide

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

    View Slide

  73. 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

    View Slide

  74. 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

    View Slide

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

    View Slide

  76. 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

    View Slide

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

    View Slide

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

    View Slide

  79. 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

    View Slide

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

    View Slide

  81. def weather_for(project)
    project.creator.address.
    country.capital.weather
    end

    View Slide

  82. MULTIPLE RESULTS

    View Slide

  83. Blog = Struct.new(:categories)
    Category = Struct.new(:posts)
    Post = Struct.new(:comments)

    View Slide

  84. 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

    View Slide

  85. 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'])
    ])
    ])
    ]

    View Slide

  86. >> 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"]

    View Slide

  87. 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

    View Slide

  88. Many = Struct.new(:values) do
    def and_then(&block)
    Many.new(values.map(&block).flat_map(&:values))
    end
    end

    View Slide

  89. 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

    View Slide

  90. 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

    View Slide

  91. 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

    View Slide

  92. 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

    View Slide

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

    View Slide

  94. 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

    View Slide

  95. def words_in(blogs)
    Many.new(blogs).
    categories.
    posts.
    comments.
    split(/\s+/).
    values
    end

    View Slide

  96. def words_in(blogs)
    Many.new(blogs).
    categories.posts.comments.split(/\s+/).
    values
    end

    View Slide

  97. def words_in(blogs)
    blogs.categories.posts.comments.split(/\s+/)
    end

    View Slide

  98. Many = Struct.new(:values) do
    def and_then(&block)
    Many.new(values.map(&block).flat_map(&:values))
    end
    !
    def method_missing(*args, &block)
    and_then do |value|
    Many.new(value.public_send(*args, &block))
    end
    end
    end

    View Slide

  99. ASYNCHRONOUS CODE

    View Slide

  100. GET https://api.github.com/

    View Slide

  101. {
    "current_user_url": "https://api.github.com/user",
    "authorizations_url": "https://api.github.com/authorizations",
    "emails_url": "https://api.github.com/user/emails",
    "emojis_url": "https://api.github.com/emojis",
    "events_url": "https://api.github.com/events",
    "feeds_url": "https://api.github.com/feeds",
    "following_url": "https://api.github.com/user/following{/target}",
    "gists_url": "https://api.github.com/gists{/gist_id}",
    "hub_url": "https://api.github.com/hub",
    "issues_url": "https://api.github.com/issues",
    "keys_url": "https://api.github.com/user/keys",
    "notifications_url": "https://api.github.com/notifications",
    "organization_url": "https://api.github.com/orgs/{org}",
    "public_gists_url": "https://api.github.com/gists/public",
    "rate_limit_url": "https://api.github.com/rate_limit",
    "repository_url": "https://api.github.com/repos/{owner}/{repo}",
    "starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
    "starred_gists_url": "https://api.github.com/gists/starred",
    "team_url": "https://api.github.com/teams",
    "user_url": "https://api.github.com/users/{user}"
    }

    View Slide

  102. GET https://api.github.com/
    GET https://api.github.com/orgs/ruby
    GET https://api.github.com/orgs/ruby/repos
    GET https://api.github.com/repos/ruby/ruby
    GET https://api.github.com/repos/ruby/ruby/contributors
    GET https://api.github.com/users/nobu

    View Slide

  103. {
    "login": "nobu",

    "name": "Nobuyoshi Nakada",

    }

    View Slide

  104. def get_json(url, &success)
    Thread.new do
    uri = URI.parse(url)
    json = Net::HTTP.get(uri)
    value = JSON.parse(json)
    success.call(value)
    end
    end

    View Slide

  105. 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

    View Slide

  106. Eventually = Struct.new(:block) do
    def initialize(&block)
    super(block)
    end
    !
    def run(&success)
    block.call(success)
    end
    end

    View Slide

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

    View Slide

  108. 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

    View Slide

  109. 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

    View Slide

  110. def get_github_api_urls
    github_root_url = 'https://api.github.com/'
    !
    Eventually.new { |success| get_json(github_root_url, &success) }
    end

    View Slide

  111. 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

    View Slide

  112. get_github_api_urls.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

    View Slide

  113. 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

    View Slide

  114. get_github_api_urls.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

    View Slide

  115. get_github_api_urls.and_then do |urls|
    !
    !
    !
    get_org(urls, 'ruby').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

    View Slide

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

    View Slide

  117. get_github_api_urls.and_then do |urls|
    !
    !
    !
    get_org(urls, 'ruby').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

    View Slide

  118. get_github_api_urls.and_then do |urls|
    !
    !
    !
    get_org(urls, 'ruby').and_then do |org|
    !
    !
    get_repos(org).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

    View Slide

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

    View Slide

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

    View Slide

  121. 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

    View Slide

  122. 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

    View Slide

  123. 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

    View Slide

  124. SO… WHAT?

    View Slide

  125. Optional = Struct.new(:value) do
    def and_then(&block)
    if value.nil?
    Optional.new(nil)
    else
    block.call(value)
    end
    end
    end
    Eventually = Struct.new(:block) do
    def and_then(&block)
    Eventually.new do |success|
    run do |value|
    block.call(value).run(&success)
    end
    end
    end
    end
    Many = Struct.new(:values) do
    def and_then(&block)
    Many.new(
    values.map(&block).flat_map(&:values)
    )
    end
    end

    View Slide

  126. Optional, Many &
    Eventually
    ARE MONADS

    View Slide

  127. ABSTRACT DATA TYPE

    View Slide

  128. SOME OPERATIONS

    View Slide

  129. #and_then
    .from_value

    View Slide

  130. def Optional.from_value(value)
    Optional.new(value)
    end
    !
    def Many.from_value(value)
    Many.new([value])
    end
    !
    def Eventually.from_value(value)
    Eventually.new { |success| success.call(value) }
    end

    View Slide

  131. SOME RULES

    View Slide

  132. #and_then
    calls a block with a value
    zero or more times
    at some point

    View Slide

  133. #and_then
    returns an instance
    of the same monad

    View Slide

  134. Optional = Struct.new(:value) do
    def and_then(&block)
    if value.nil?
    Optional.new(nil)
    else
    block.call(value)
    end
    end
    end
    Eventually = Struct.new(:block) do
    def and_then(&block)
    Eventually.new do |success|
    run do |value|
    block.call(value).run(&success)
    end
    end
    end
    end
    Many = Struct.new(:values) do
    def and_then(&block)
    Many.new(
    values.map(&block).flat_map(&:values)
    )
    end
    end

    View Slide

  135. #and_then
    and .from_value
    don’t mess with the value

    View Slide

  136. COMMON INTERFACE

    View Slide

  137. #and_then
    lets us connect together
    a sequence of operations

    View Slide

  138. Optional#and_then:
    do the next thing
    if the value isn’t nil

    View Slide

  139. Many#and_then:
    do the next thing
    once for each value

    View Slide

  140. Eventually#and_then:
    do the next thing
    whenever the value is ready

    View Slide

  141. SHARED
    FUNCTIONALITY

    View Slide

  142. #within

    View Slide

  143. module Monad
    def within(&block)
    and_then do |value|
    self.class.from_value(block.call(value))
    end
    end
    end
    !
    Optional.include(Monad)
    Many.include(Monad)
    Eventually.include(Monad)

    View Slide

  144. we can write code that
    uses #within
    without knowing which
    monad it is

    View Slide

  145. def description_from(containing_json)
    containing_json.within do |json|
    JSON.parse(json)
    end.within do |hash|
    "#{hash['name']} (#{hash['login']})"
    end
    end

    View Slide

  146. >> optional_json = Optional.new('{ "login": "nobu", "name": "Nobuyoshi Nakada" }')
    => #
    >> optional_description = description_from(optional_json)
    => #
    >> optional_description.value
    => "Nobuyoshi Nakada (nobu)"

    View Slide

  147. >> optional_json = Optional.new(nil)
    => #
    >> optional_description = description_from(optional_json)
    => #
    >> optional_description.value
    => nil

    View Slide

  148. >> many_jsons = Many.new([
    '{ "login": "nobu", "name": "Nobuyoshi Nakada" }',
    '{ "login": "matz", "name": "Yukihiro Matsumoto" }'
    ])
    => #"{ \"login\": \"nobu\", \"name\": \"Nobuyoshi Nakada\" }",
    "{ \"login\": \"matz\", \"name\": \"Yukihiro Matsumoto\" }"
    ]>
    >> many_descriptions = description_from(many_jsons)
    => #"Nobuyoshi Nakada (nobu)",
    "Yukihiro Matsumoto (matz)"
    ]>
    >> many_descriptions.values
    => ["Nobuyoshi Nakada (nobu)", "Yukihiro Matsumoto (matz)"]

    View Slide

  149. >> eventually_json = Eventually.new do |success|
    Thread.new do
    uri = URI.parse('https://api.github.com/users/nobu')
    json = Net::HTTP.get(uri)
    success.call(json)
    end
    end
    => #>
    >> eventually_description = description_from(eventually_json)
    => #>
    >> eventually_description.run { |description| puts description }
    => #
    Nobuyoshi Nakada (nobu)

    View Slide

  150. def description_from(containing_json)
    containing_json.within do |json|
    JSON.parse(json)
    end.within do |hash|
    "#{hash['name']} (#{hash['login']})"
    end
    end

    View Slide

  151. (ABSTRACT,
    BUT GOOD!)

    View Slide

  152. https://github.com/tomstuart/monads
    gem install monads

    View Slide

  153. Stuart
    Understanding Computation
    From Simple Machines to Impossible Programs
    Tom Stuart
    Understanding
    Computation
    .com
    @oreillymedia
    k.com/oreilly
    uart, a computer
    t and programmer,
    ounder of Codon, a
    product consultancy
    don. He works as a
    ant, mentor, and
    helping companies
    ove the quality and
    of their approach to
    g software products.
    http://computationbook.com/

    View Slide

  154. thanks!
    @tomstuart • [email protected] • computationbook.com
    https://github.com/tomstuart/monads
    gem install monads

    View Slide