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. 4.
  2. 7.
  3. 9.

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

    { return “hi “ + x; } mapping of inputs to outputs: f(x) = x2
  4. 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
  5. 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
  6. 13.

    Stabby Lambdas? lambda { |x| “hi “ + x }

    ->(x) { “hi “ + x } ...because more syntax never hurt anybody. These are equivalent:
  7. 14.

    3 tricks: 1. Passing Functions: the Strategy Pattern 2. Use

    Functions to Model Actions 3. Functional Composition: Build Methods from Functions
  8. 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? }
  9. 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 }
  10. 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
  11. 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.
  12. 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
  13. 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
  14. 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
  15. 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.
  16. 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”
  17. 28.
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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.
  23. 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
  24. 37.

    2 ingredients: you can make functions from other functions you

    can pass blocks and functions to define_method
  25. 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
  26. 40.

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

    = ->(x) { x.reverse } reverse.call(‘drawer’) #=> “reward” combo = upcase | reverse combo.call(‘stressed‘) #=> “DESSERTS“
  27. 41.
  28. 42.
  29. 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
  30. 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
  31. 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
  32. 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
  33. 49.

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

    add_bang ->(input) { “#{input}!” } end
  34. 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
  35. 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
  36. 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.