Object-Functional Fusion in Ruby

Object-Functional Fusion in Ruby

We all know Ruby supports functional style programming: it’s got blocks! But it’s still, at bottom, an object-oriented language. What are some ways we can combine OO and FP to good effect?

360300f6962b973dae989991dc204f75?s=128

Dan Bernier

June 15, 2013
Tweet

Transcript

  1. Object- Functional Fusion in Ruby RubyNation 2013 @danbernier

  2. http://bit.ly/offirrn2013 Object Functional Fusion In Ruby Ruby Nation 2013

  3. i — Programming languages

  4. rubyists SOLID Demeter Method decomposition Messages, not Methods Stubs &

    Mocks Presenters Roles & Responsibilities —
  5. rubyists Closures Functional Composition Currying Laziness (~) Passing functions Returning

    functions ?
  6. combine OO and FP to good effect

  7. None
  8. what’s a function? lambda? block? proc?

  9. image → grayscale bread → toast what’s a function? function(x)

    { return “hi “ + x; } mapping of inputs to outputs: f(x) = x2
  10. λ what’s a lambda? Alonzo Church says: “it’s a function.”

    “It’s syntax for making a Proc,” says Ruby. lambda { 0 }.class #=> Proc
  11. so, what’s a Proc? Ruby says, “It’s a function.”

  12. then what’s a block? “It’s syntax for making a Proc,

    but only at the end of a method,” says Ruby. def foo(&block) block.class #=> Proc end
  13. Stabby Lambdas? lambda { |x| “hi “ + x }

    ->(x) { “hi “ + x } ...because more syntax never hurt anybody. These are equivalent:
  14. 3 tricks: 1. Passing Functions: the Strategy Pattern 2. Use

    Functions to Model Actions 3. Functional Composition: Build Methods from Functions
  15. Passing functions: the Strategy Pattern a warm-up

  16. def lrkvo_rjiluyjg(dqdok) wre_uqikmtzx = [] iyb_hehqmgco = [] if oskzk.smsxvqix.gjnk

    > 4 upewo = 0 jnvwe.ysjvraqc.ywifomwbueu.lrzr{|sourlio| if jgwkhcw.qkkpygo_sw < 3.agpi.sfu yv ukrhw == 4 bxm_xomkpioi << hixryxa else zkw_hkvcvazw << huqvuni gatha += 1 end } if zgm_xmpdqfez.mjfx == 0 tnp_jxiynozv = yxd_xzwmxooz.tzem(4) uni_juovblzf = jvm_gzqyvozw.benf(4) end else mssjk.pgsgjsgg.mcgzkfpqrhb.jsuz{|bniebnk| eqt_fispkfwu << jobpjcp } end qsvioh ivh_poyzmyfw, exi_jgafukxk end λ parameterize? }
  17. users.sort_by { |user| user.name } users.sort_by { |user| user.karma_points }

    users.sort_by { |user| user.posts.count } users.sort_by { |user| user.created_at }
  18. [1,2,3].map { |n| n * 4 } #=> [4,8,12] [1,2,3].select

    { |n| n.even? } #=> [2] [1,2,3].any?(&:odd?) #=> true [1,2,3].all?(&:odd?) #=> false %w[hey you guys].max_by(&:size) #=> “guys” Users.all.each(&:destroy) #=> empty table
  19. The First Trick: When working with a collection, SEPARATE the

    item-handling from the iteration. Pass the item-handler as a function to a method that does the iteration.
  20. Let’s make a twitter- bot

  21. class TwitterBotBuilder def follow_more_tweeters 1000.times do tweeter = @twitter_api.pick_random_user if

    tweeter.profile =~ /(ruby|rails|cats)/i follow!(tweeter) end end end end twitter_bot_builder.follow_more_tweeters
  22. class TwitterBotBuilder def follow_more_tweeters 1000.times do tweeter = @twitter_api.pick_random_user if

    tweeter.follows?(‘tenderlove’) follow!(tweeter) end end end end twitter_bot_builder.follow_more_tweeters
  23. twitter_bot_builder.follow_tweeter_if do |tweeter| tweeter.follows?(‘tenderlove’) end

  24. class TwitterBotBuilder def follow_tweeter_if(&follow_criteria) 1000.times do tweeter = @twitter_api.pick_random_user if

    follow_criteria.call(tweeter) follow!(tweeter) end end end end twitter_bot_builder.follow_tweeter_if do |tweeter| tweeter.recent_tweets(50).grep(/rubynation/) end
  25. The First Trick (again): When working with an API, SEPARATE

    the data-handling from the API interaction. Pass the data-handler as a function to a method that calls the API.
  26. Use Functions to Model Actions watch your steps!

  27. Washington, DC ˒ ˒ ˒ ˒ ˒ ˒ ˒ ˒

    ˒ ˒ “Barack, thanks for reporting that issue!” “Barack reported an issue in your watch area” “Barack reported an issue near issues you’ve reported”
  28. None
  29. IssueMailer.thank_reporter(issue).deliver WatchArea.around(issue).each do |watch_area| IssueMailer.tell_watch_area(watch_area, issue).deliver end Recommendation.for(issue).each do |user|

    IssueMailer.recommend(user, issue).deliver end
  30. emails = [] IssueMailer.thank_reporter(issue).deliver emails << issue.reporter.email WatchArea.around(issue).each do |watch_area|

    unless emails.include?(watcher.email) IssueMailer.tell_watch_area(watch_area, issue).deliver emails << watcher.email end end Recommendation.for(issue).each do |user| unless emails.include?(user.email) IssueMailer.recommend(user, issue).deliver emails << user.email end end
  31. mail = MailGuard.new mail.guard(issue.reporter.email) do IssueMailer.thank_reporter(issue).deliver end WatchArea.around(issue).each do |watch_area|

    mail.guard(watcher.email) do IssueMailer.tell_watch_area(watch_area, issue).deliver end end Recommendation.for(issue).each do |user| mail.guard(user.email) do IssueMailer.recommend(user, issue).deliver end end
  32. class MailGuard def initialize @already_sent = [] end def guard(address,

    &mailer) unless @already_sent.include?(address) mailer.call @already_sent << address end end end
  33. mail = MailGuard.new mail.guard(issue.reporter.email, :thank_reporter) do IssueMailer.thank_reporter(issue).deliver end WatchArea.around(issue).each do

    |watch_area| mail.guard(watcher.email, :tell_watcher) do IssueMailer.tell_watch_area(watch_area, issue).deliver end end Recommendation.for(issue).each do |user| mail.guard(user.email, :recommend) IssueMailer.recommend(user, issue).deliver end end
  34. The Second Trick: SEPARATE the chain of steps from deciding

    whether to take each step. Pass each step as a function to an object that decides whether to call it.
  35. • Separate iteration: pass a function to handle each item

    • Separate API interaction: pass a function to handle the data • Separate the actions from deciding whether to take them: pass them as functions s e p a r a t e
  36. Build methods from functions class Functional def λ def λ

    def λ def λ end
  37. 2 ingredients: you can make functions from other functions you

    can pass blocks and functions to define_method
  38. class MethodFromBlock define_method(:double) do |x| x * 2 end end

    class MethodFromFunction define_method(:double, ->(x) { x * 2 }) end class MethodFromFunctionVariable double = ->(x) { x * 2 } define_method(:double, double) end
  39. $ git grep cats | wc -l

  40. upcase = ->(x) { x.upcase } upcase.call(‘yelling‘) #=> “YELLING“ reverse

    = ->(x) { x.reverse } reverse.call(‘drawer’) #=> “reward” combo = upcase | reverse combo.call(‘stressed‘) #=> “DESSERTS“
  41. None
  42. None
  43. class Proc def self.compose(first, second) ->(input) { first_result = first.call(input)

    second.call(first_result) } end end def |(next_proc) Proc.compose(self, next_proc) end
  44. Put TheM Together!

  45. # prepend ‘oy,’, yell it # add bang, append ‘wat

    up dood?’ # add bang, prepend ‘vote for’, yell it class Yeller def oy(name) [‘oy,’, name].join(‘ ‘).upcase end def wat_up(name) [“#{name}!”, ‘wat up dood?’].join(‘ ‘) end def vote_for(name) [‘vote for’, “#{name}!”].join(‘ ‘).upcase end end
  46. # prepend ‘oy,’, yell it # add bang, append ‘wat

    up dood?’ # add bang, prepend ‘vote for’, yell it prepend(‘oy,’) | yell_it add_bang | append(‘wat up dood?’) add_bang | prepend(‘vote for’) | yell_it
  47. def yell_it ->(input) { input.upcase } end

  48. class Prepender def initialize(msg) @msg = msg end def call(input)

    [@msg, input].join(‘ ‘) end end def prepend(msg) ->(input) { [msg, input].join(‘ ‘) } end
  49. def append(msg) ->(input) { [input, msg].join(‘ ‘) } end def

    add_bang ->(input) { “#{input}!” } end
  50. class Yeller extend YellingFunctions define_method :oy, prepend(‘oy,’) | yell_it define_method

    :wat_up, add_bang | append(‘wat up dood?’) define_method :vote_for, add_bang | prepend(‘vote for’) | yell_it end
  51. class PersonValidator extend Validator define_method :name, required | length(0..50) define_method

    :age, required | numeric(0..130) define_method :phone, required | format(/\d{3}-\d{4}/) end class CupcakeValidator extend Validator define_method :base, required | in(‘yellow’, ‘chocolate’) define_method :icing, required | in(‘vanilla’, ‘chocolate’) define_method :toppings, any(‘sprinkles’, ‘fondant’) end
  52. The Third Trick: When building lots of methods do the

    same kinds of things, define the methods by combining functions. It’s a DSL for your methods.
  53. Reg Braithwaite’s old Functional/Ruby posts Brian Marick’s “Functional Programming for

    the Object-Oriented Programmer”
  54. aka “SICP” “The Little Schemer,” with Space for Jelly Stains!

  55. Come say hi! @danbernier Thank You! ?