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

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?

Avatar for Dan Bernier

Dan Bernier

June 15, 2013
Tweet

Other Decks in Programming

Transcript

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

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

    “It’s syntax for making a Proc,” says Ruby. lambda { 0 }.class #=> Proc
  3. 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
  4. Stabby Lambdas? lambda { |x| “hi “ + x }

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

    Functions to Model Actions 3. Functional Composition: Build Methods from Functions
  6. 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? }
  7. 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 }
  8. [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
  9. 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.
  10. 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
  11. 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
  12. 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
  13. 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.
  14. 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”
  15. 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
  16. 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
  17. class MailGuard def initialize @already_sent = [] end def guard(address,

    &mailer) unless @already_sent.include?(address) mailer.call @already_sent << address end end end
  18. 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
  19. 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.
  20. • 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
  21. 2 ingredients: you can make functions from other functions you

    can pass blocks and functions to define_method
  22. 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
  23. upcase = ->(x) { x.upcase } upcase.call(‘yelling‘) #=> “YELLING“ reverse

    = ->(x) { x.reverse } reverse.call(‘drawer’) #=> “reward” combo = upcase | reverse combo.call(‘stressed‘) #=> “DESSERTS“
  24. 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
  25. # 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
  26. # 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
  27. class Prepender def initialize(msg) @msg = msg end def call(input)

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

    add_bang ->(input) { “#{input}!” } end
  29. 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
  30. 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
  31. 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.