Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. 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
  2. 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
  3. >> 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"
  4. module Stack def size if empty? 0 else pop.size +

    1 end end end ! ArrayStack.include(Stack) LinkedListStack.include(Stack)
  5. #each calls a block with a value zero or more

    times in immediate sequence
  6. class GeneratedCollection def each(&block) number = 1 while number <=

    5 block.call(number) number = number + 1 end end end
  7. >> 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
  8. 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)
  9. >> HardcodedCollection.new.select { |n| n.odd? } => [1, 3, 5]

    ! >> GeneratedCollection.new.select { |n| n.odd? } => [1, 3, 5]
  10. >> 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 …>
  11. >> bad_project = Project.new(Person.new(Address.new(nil))) => #<struct Project …> ! >>

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

    address.country capital = country.capital weather = capital.weather end
  13. 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
  14. 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
  15. # 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
  16. 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
  17. 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
  18. # 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
  19. >> 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
  20. >> 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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']) ]) ]) ]
  32. >> 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"]
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. { "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}" }
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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)
  60. >> 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)"
  61. >> optional_json = Optional.new(nil) => #<struct Optional value=nil> >> optional_description

    = description_from(optional_json) => #<struct Optional value=nil> >> optional_description.value => nil
  62. >> 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)"]
  63. >> 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)
  64. 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/