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 http://codon.com/refactoring-ruby-with-monads.

Cd9b247e4507fed75312e9a42070125d?s=128

Tom Stuart

August 02, 2014
Tweet

Transcript

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

  2. WARM UP

  3. ANALOGIES

  4. WHAT’S A STACK?

  5. A KIND OF VALUE…

  6. WITH CERTAIN OPERATIONS … …

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

  8. THAT FOLLOW CERTAIN RULES …

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

    == false
  10. WE CAN IMPLEMENT THEM HOWEVER WE LIKE

  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
  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
  13. WE CAN USE THEM WITHOUT KNOWING THEIR IMPLEMENTATION

  14. >> stack = ArrayStack.empty => #<struct ArrayStack values=[]> >> stack.push('hello').push('world').pop.top

    => "hello" >> stack = LinkedListStack.empty => #<struct LinkedListStack top=nil, rest=nil> >> stack.push('hello').push('world').pop.top => "hello"
  15. WE CAN DEFINE MORE OPERATIONS

  16. module Stack def size if empty? 0 else pop.size +

    1 end end end ! ArrayStack.include(Stack) LinkedListStack.include(Stack)
  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
  18. OPERATIONS & RULES

  19. COMMON INTERFACE & SHARED FUNCTIONALITY

  20. WHAT’S A COLLECTION?

  21. A KIND OF VALUE…

  22. WITH CERTAIN OPERATIONS … …

  23. #each

  24. THAT FOLLOW CERTAIN RULES …

  25. #each calls a block with a value zero or more

    times in immediate sequence
  26. WE CAN IMPLEMENT IT HOWEVER WE LIKE

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

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

    5 block.call(number) number = number + 1 end end end
  29. WE CAN USE IT WITHOUT KNOWING ITS IMPLEMENTATION

  30. >> collection = HardcodedCollection.new => #<HardcodedCollection> >> collection.each { |n|

    puts n } 1 2 3 4 5 => nil >> collection = GeneratedCollection.new => #<GeneratedCollection> >> collection.each { |n| puts n } 1 2 3 4 5 => nil
  31. WE CAN DEFINE MORE OPERATIONS

  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)
  33. >> HardcodedCollection.new.select { |n| n.odd? } => [1, 3, 5]

    ! >> GeneratedCollection.new.select { |n| n.odd? } => [1, 3, 5]
  34. OPERATIONS & RULES

  35. COMMON INTERFACE & SHARED FUNCTIONALITY

  36. WHAT ARE STACKS & COLLECTIONS?

  37. ABSTRACT DATA TYPE

  38. (ABSTRACT, BUT GOOD!)

  39. LET’S DO SOME REFACTORING!

  40. HANDLING NIL

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

    = Struct.new(:capital) City = Struct.new(:weather)
  42. def weather_for(project) project.creator.address. country.capital.weather end

  43. >> city = City.new('sunny') => #<struct City …> ! >>

    country = Country.new(city) => #<struct Country …> ! >> address = Address.new(country) => #<struct Address …> ! >> person = Person.new(address) => #<struct Person …> ! >> project = Project.new(person) => #<struct Project …>
  44. >> weather_for(project) => "sunny"

  45. >> bad_project = Project.new(Person.new(Address.new(nil))) => #<struct Project …> ! >>

    weather_for(bad_project) NoMethodError: undefined method `capital' for nil:NilClass
  46. def weather_for(project) project.creator.address. country.capital.weather end

  47. def weather_for(project) creator = project.creator address = creator.address country =

    address.country capital = country.capital weather = capital.weather end
  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
  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
  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
  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
  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
  53. def weather_for(project) project. try(:creator). try(:address). try(:country). try(:capital). try(:weather) end

  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
  55. class Object remove_method :try end ! class NilClass remove_method :try

    end
  56. Optional = Struct.new(:value)

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

    => "hello" >> optional_string = Optional.new(nil) => #<struct Optional value=nil> >> optional_string.value => nil
  58. class Optional def try(*args, &block) if value.nil? nil else value.public_send(*args,

    &block) end end end
  59. >> optional_string = Optional.new('hello') => #<struct Optional value="hello"> >> length

    = optional_string.try(:length) => 5 >> optional_string = Optional.new(nil) => #<struct Optional value=nil> >> length = optional_string.try(:length) => nil
  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
  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
  62. class Optional def try(*args, &block) if value.nil? nil else value.public_send(*args,

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

    end end
  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
  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
  66. >> weather_for(project) => "sunny" ! >> weather_for(bad_project) NoMethodError: undefined method

    `capital' for nil:NilClass
  67. class Optional def try(&block) if value.nil? nil else block.call(value) end

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

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

  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
  71. class Optional def try(&block) if value.nil? Optional.new(nil) else block.call(value) end

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

    end end
  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
  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
  75. class Optional def method_missing(*args, &block) and_then do |value| Optional.new(value.public_send(*args, &block))

    end end end
  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
  77. def weather_for(project) Optional.new(project). creator. address. country. capital. weather. value end

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

  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
  80. def weather_for(project) Optional.new(project). creator.address.country.capital.weather. value end

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

  82. MULTIPLE RESULTS

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

  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
  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']) ]) ]) ]
  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"]
  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
  88. Many = Struct.new(:values) do def and_then(&block) Many.new(values.map(&block).flat_map(&:values)) end end

  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
  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
  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
  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
  93. class Many def method_missing(*args, &block) and_then do |value| Many.new(value.public_send(*args, &block))

    end end end
  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
  95. def words_in(blogs) Many.new(blogs). categories. posts. comments. split(/\s+/). values end

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

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

  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
  99. ASYNCHRONOUS CODE

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

  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}" }
  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
  103. { "login": "nobu", … "name": "Nobuyoshi Nakada", … }

  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
  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
  106. Eventually = Struct.new(:block) do def initialize(&block) super(block) end ! def

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

    block.call(value).run(&success) end end end end
  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
  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
  110. def get_github_api_urls github_root_url = 'https://api.github.com/' ! Eventually.new { |success| get_json(github_root_url,

    &success) } end
  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
  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
  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
  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
  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
  116. def get_repos(org) repos_url = org['repos_url'] ! Eventually.new { |success| get_json(repos_url,

    &success) } end
  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
  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
  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
  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
  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
  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
  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
  124. SO… WHAT?

  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
  126. Optional, Many & Eventually ARE MONADS

  127. ABSTRACT DATA TYPE

  128. SOME OPERATIONS

  129. #and_then .from_value

  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
  131. SOME RULES

  132. #and_then calls a block with a value zero or more

    times at some point
  133. #and_then returns an instance of the same monad

  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
  135. #and_then and .from_value don’t mess with the value

  136. COMMON INTERFACE

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

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

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

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

  141. SHARED FUNCTIONALITY

  142. #within

  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)
  144. we can write code that uses #within without knowing which

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

    (#{hash['login']})" end end
  146. >> optional_json = Optional.new('{ "login": "nobu", "name": "Nobuyoshi Nakada" }')

    => #<struct Optional value="{ \"login\": \"nobu\", \"name\": \"Nobuyoshi Nakada\" }"> >> optional_description = description_from(optional_json) => #<struct Optional value="Nobuyoshi Nakada (nobu)"> >> optional_description.value => "Nobuyoshi Nakada (nobu)"
  147. >> optional_json = Optional.new(nil) => #<struct Optional value=nil> >> optional_description

    = description_from(optional_json) => #<struct Optional value=nil> >> optional_description.value => nil
  148. >> many_jsons = Many.new([ '{ "login": "nobu", "name": "Nobuyoshi Nakada"

    }', '{ "login": "matz", "name": "Yukihiro Matsumoto" }' ]) => #<struct Many values=[ "{ \"login\": \"nobu\", \"name\": \"Nobuyoshi Nakada\" }", "{ \"login\": \"matz\", \"name\": \"Yukihiro Matsumoto\" }" ]> >> many_descriptions = description_from(many_jsons) => #<struct Many values=[ "Nobuyoshi Nakada (nobu)", "Yukihiro Matsumoto (matz)" ]> >> many_descriptions.values => ["Nobuyoshi Nakada (nobu)", "Yukihiro Matsumoto (matz)"]
  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 => #<struct Eventually block=#<Proc>> >> eventually_description = description_from(eventually_json) => #<struct Eventually block=#<Proc>> >> eventually_description.run { |description| puts description } => #<Thread run> Nobuyoshi Nakada (nobu)
  150. def description_from(containing_json) containing_json.within do |json| JSON.parse(json) end.within do |hash| "#{hash['name']}

    (#{hash['login']})" end end
  151. (ABSTRACT, BUT GOOD!)

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

  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/
  154. thanks! @tomstuart • tom@codon.com • computationbook.com https://github.com/tomstuart/monads gem install monads