Are Ruby's Refinements the best idea since blocks and modules, or a terrible mistake? Decide for yourself. (Includes presenter's notes right on the slides. If you want a version without the notes, for presenting it yourself - just mail me at paolo.nusco.perrotta on Gmail).
11. class String
alias_method :old_length, :length
def length
old_length > 5 ? "long" : "short"
end
end
"War and Peace".length # => "long"
!This example also shows why monkeypatches are dangerous: because they are global.
20. class C
using StringExtensions
"hello".shout # => "HELLO!"
end
C.class_eval do
"hello".shout # => "HELLO!"
end
dynamic scope
The original proposal (Dynamically Scoped Refinements) says yes, in all three cases.
22. confusing code
def add(x, y)
x + y
end
SomeClass.class_eval do
add(1, 1) # => 2
end
SomeOtherClass.class_eval do
add(1, 1) # => "11"
end
using FloatPointMath # refines Fixnum#+
add(1, 1) # => 2.0
You need to look at the implementations to understand the interface.
23. slows down the language
def add(x, y)
x + y
end
SomeClass.class_eval do
add(1, 1) # => 2
end
SomeOtherClass.class_eval do
add(1, 1) # => "11"
end
using FloatPointMath # refines Fixnum#+
add(1, 1) # => 2.0
The interpreter also needs to do the same, so Refinements can slow down the entire interpreter.
24. security threat
def add(x, y)
x + y
end
SomeClass.class_eval do
add(1, 1) # => 2
end
SomeOtherClass.class_eval do
add(1, 1) # => "11"
end
using FloatPointMath # refines Fixnum#+
add(1, 1) # => 2.0
Less understanding potentially means less security.
25. surprising corner cases
def add(x, y)
x + y
end
SomeClass.class_eval do
add(1, 1) # => 2
end
SomeOtherClass.class_eval do
add(1, 1) # => "11"
end
using FloatPointMath # refines Fixnum#+
add(1, 1) # => 2.0
Some things might not work as you expect. (For example, the last line doesn’t work in irb).
27. •they make Ruby code potentially confusing
•they impact performance
•they impact security
•they have weird corner cases
refinements: the bad_
The bad of Dynamically Scoped Refinements.
31. module StringExtensions
refine String do
def shout
upcase + "!"
end
end
end
module ModuleThatUsesTheRefinement
using StringExtensions
"hello".shout # => "HELLO!"
end
This stuff is the same…
33. lexical scope
class C
using StringExtensions
"hello".shout # => "HELLO!"
end
C.class_eval do
"hello".shout # => NoMethodError
end
No matter how you change the scope, Refinements *only* work in the lexical scope.
34. (almost) no confusion
def add(x, y)
x + y
end
SomeClass.class_eval do
add(1, 1) # => 2
end
SomeOtherClass.class_eval do
add(1, 1) # => 2
end
using FloatPointMath # refines Fixnum#+
add(1, 1) # => 2
(Note that the very last line might still surprise you, until you wrap your head around lexical scoping).
35. the three use cases again
But how do these new Refinements apply to the three main use cases of Monkeypatching?
36. describe Fixnum do
it("can be added") do
(2 + 2).should == 4
end
end
# => NoMethodError (undefined method 'it')
domain specific languages
This doesn’t work anymore.
37. class MyClass < ActiveRecord::Base
2.hours
end
# => NoMethodError (undefined method 'hours')
convenience methods
Neither does this.
38. class MyClass < ActiveRecord::Base
using ActiveSupport::CoreExtensions
2.hours # => 7200 seconds
end
convenience methods
(Unless I use using() in each and every class where I want the refinements - not very DRY).
39. module StringExtensions
refine String do
def length
super > 5 ? "long" : "short"
end
end
end
using StringExtensions
"War and Peace".length # => "long"
method wrappers
This one does work, thans to the way “super” works in Refinements.
40. •they don’t fix monkeypatches in general
•they still have weird corner cases
refinements today: the bad_
The bad of Lexically Scoped Refinements.
41. •they do fix some monkeypatching cases
•they don’t make the code confusing
•they don’t impact performance or security
•…and besides, they open the road for more
refinements today: the good
The good of Lexically Scoped Refinements.
43. describe Fixnum do
it "can be added" do
(2 + 2).should == 4
end
end
This code relies on multiple features that do not seem to make much sense (singleton classes, optional parentheses), but ended up being “abused” by the
community. It is hard to plan for this.
44. language design
is a deep problem
The point of this entire speech: we cannot plan. We need to experiment.
“Obvious” problems are those that you can solve yourself, if you have some time and knowledge.
“Non-obvious” problems require more effort. You might not know how to solve them at first.
“Deep” problems might not even have an optimal solution. You have to experiment.
Let’s look at the problem that refinements are designed to solve.
In Ruby, you can do this.
Three of the most important use cases for monkeypatching.
This example also shows why monkeypatches are dangerous: because they are global.
This is what we want, and what Refinements are.
Let’s look at Refinements, the way they were originally conceived.
Refinement in a class/module. Only active in the marked area.
Refinement at the top level. Only active in the marked area.
…but will it work here?
…or here?
…or here?
The original proposal (Dynamically Scoped Refinements) says yes, in all three cases.
But people found potential problems with this.
You need to look at the implementations to understand the interface.
The interpreter also needs to do the same, so Refinements can slow down the entire interpreter.
Less understanding potentially means less security.
Some things might not work as you expect. (For example, the last line doesn’t work in irb).
The good of Dynamically Scoped Refinements.
The bad of Dynamically Scoped Refinements.
This is what the core team decided right before Ruby 2.0.
So we have a different versions of Refinements instead.
This stuff is the same…
…but this doesn’t work.
No matter how you change the scope, Refinements *only* work in the lexical scope.
(Note that the very last line might still surprise you, until you wrap your head around lexical scoping).
But how do these new Refinements apply to the three main use cases of Monkeypatching?
This doesn’t work anymore.
Neither does this.
(Unless I use using() in each and every class where I want the refinements - not very DRY).
This one does work, thans to the way “super” works in Refinements.
The bad of Lexically Scoped Refinements.
The good of Lexically Scoped Refinements.
This code relies on multiple features that do not seem to make much sense (singleton classes, optional parentheses), but ended up being “abused” by the community. It is hard to plan for this.
The point of this entire speech: we cannot plan. We need to experiment.