let's do ruby refinements!

ruby has a nice central metaphor

              
              receiver = Receiver.new
              receiver.message

              # or, implicit receiver
              message
              # => NameError: undefined local variable or method `message' for main:Object

              # this metaphor also applies to core types
              [1, 2, 3].first
              
            

but it has some limits...

              
              class Cat
                def meow
                  "meow"
                end
                def louder
                  upcase + " said the cat"
                end
              end

              c = Cat.new
              c.meow.louder
              # => NoMethodError: undefined method `louder' for "meow":String
              
            

solution 1: wrap it in a method

              
              class Cat
                def meow
                  "meow"
                end
                def louder(meow)
                  meow.upcase + " said the cat"
                end
              end

              c = Cat.new
              c.louder(c.meow)
              # => "MEOW said the cat"
              
            

solution 2: return self & use side-effects

              
class Cat
  attr_reader :meow
  def meow
    @meow = "meow"
    self
  end
  def louder
    @meow.upcase + " said the cat"
  end
end

c = Cat.new
c.meow.louder
# => "MEOW said the cat"
              
            

solution 3: monkeypatch


class String
  def louder
    upcase + " said the cat"
  end
end

              

class Cat
  def meow
    "meow"
  end
end

c = Cat.new
c.meow.louder
# => "MEOW said the cat"
              
            
  • solution 1: boilerplate πŸ˜«πŸ’’πŸ’”πŸ˜Ÿ
  • solution 2: boilerplate πŸ˜±πŸ’©β˜£οΈ
  • solution 3: no boilerplate!!! πŸ˜πŸ’―πŸ’―πŸ‘
monkeypatching is bad!!!!

this is bad


class Array
  def first
    if rand(1..10) == 1
      'wat'
    else
      super
    end
  end
end

fine!! i love boilerplate! ill just write that FOREVER

hold up. you can...

✨refine it✨


module Loud
  refine String do
    def louder
      upcase + " said the cat"
    end
  end
end

              

class Cat
  def meow
    "meow"
  end
end

using Loud
c = Cat.new
c.meow.louder
# => "MEOW said the cat"
              
            

refinements will respect scope


# not activated here
class Foo
  # not activated here
  def foo
    # not activated here
  end
  using M
  # activated here
  def bar
    # activated here
  end
  # activated here
end
# not activated here

(which is why it beats monkeypatching 🍌)

examples

a real life example (part one)


module HashFmap
  refine Hash do
    def fmap &block
      self.reduce({}) { |memo, (k,v)| memo.merge!({ k => block.call(v) }) }
    end
  end
end


h = {:foo=>"bar", :biz=>"baz"}
h.map {|k, v| v.upcase }
# => ["BAR", "BAZ"]

using HashFmap
h.fmap(&:upcase)
# => {:foo=>"BAR", :biz=>"BAZ"}

a real life example (part two)


module ThirdPartyAPIParams
  refine ModelOne do
    def params
      { ParamOne: 21 }
    end
  end
  refine ModelTwo do
    def params
      { ParamTwo: 22 }
    end
  end
end


class Worker
  using ThirdPartyAPIParams
  def perform(id, model)
    m = model.find(id)
    APIClient.post(m.params)
  end
end

a real life example (part three)


module MyWeirdFormatting
  refine Thing do
    def as_weird_format
      to_s.chars.map(&:ord).map {|c| c.to_s(2) }.join
    end
  end
end


class ThingService
  using MyWeirdFormatting
  attr_accessor :thing

  def initialize(thing)
    @thing = thing
  end
  def store
    $db.store(thing.id, thing.as_weird_format)
  end
end

re: why not just use a mixin or inheritence??

Do you really need every method on your class everywhere your class appears?

Really really??

My opinion: when an object has a smaller "surface area" of methods at base, and I extend its behavior explicitly in select contexts, I find it easier to reason about.


pry(main)> my_refined_object.methods
# => [ :to_s, :as_json, :inherited ]
pry(main)> my_object_bloated_with_mixins.methods
# => [ :to_s, :as_json, :random_method, :irrelevant, :wont_work, :inherited , :idc, :definitely_wrong, :mysterious ]

Literally, pry into some code and call .methods on it. What's truly useful for your receiver to accept as a message in that calling context?

thanks for listening!

questions?