forked from judofyr/timeless
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
created_at: Sun Nov 28 16:14:40 +0100 2010 | ||
entry: refinements-in-ruby | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,385 @@ | ||
Title: Refinements in Ruby | ||
Subtitle: Monkey patching for friendly monkeys | ||
HTML use syntax: true | ||
|
||
At RubyConf 2010 Shugo Maeda [talked about *Refinements*][rc-talk]: A proposal | ||
for a new feature in Ruby which allows you to easily override methods without | ||
affecting other parts of your program: | ||
|
||
module TimeExtensions | ||
refine Fixnum do | ||
def minutes; self * 60; end | ||
end | ||
end | ||
|
||
class MyApp | ||
using TimeExtensions | ||
|
||
def initialize | ||
p 2.minutes | ||
end | ||
end | ||
|
||
MyApp.new # => 120 | ||
p 2.minutes # => NoMethodError | ||
{: lang=ruby } | ||
|
||
Let's have a look at the *why's* and *how's* of this proposal. | ||
|
||
(snip) | ||
|
||
## The Power of Monkey Patching | ||
|
||
Ruby allows you to both open previously defined classes and redefine any | ||
method. In addition, Ruby doesn't treat core classes any differently from | ||
user-defined classes, so this gives you a lot of power to completely change the | ||
behaviour Ruby. This is of course a double edged sword: You can more easily | ||
change Ruby to match your thoughts (rather than changing your thoughts to match | ||
Ruby), but it also means that everyone else now needs to follow your rules. | ||
|
||
Getting everyone to play along nicely has proven to be a challenge, and the | ||
solution has always been solved socially. As long as two core teams (let's say | ||
Rails and DataMapper) work together, they can quite easily solve any problems, | ||
but the real issue is when you, as a user, want to use two libraries together. | ||
The libraries may work perfectly separately, but the moment you combine them | ||
you'll get some weird behaviour. There's not really much to can do, other than | ||
waiting for the library to be updated (or do the work yourself). | ||
|
||
## A wild Classbox appears! | ||
|
||
If you've been following the development of Ruby, you may have heard of | ||
[*classboxes*][classbox]. They were first introduced by Alexandre Bergel, | ||
Stéphane Ducasse, and Roel Wuyts in 2003 by the paper [Classboxes: A Minimal | ||
Module Model Supporting Local Rebinding][classbox-paper]. It's essentially a | ||
way to monkey patch classes and methods, but only within the context of *your* | ||
code and not globally. At the moment, it's been implemented in Smalltalk | ||
(Squak), Java and .Net, but there's also been some work at trying to apply it | ||
to Ruby. | ||
|
||
The refinements proposal by Shugo captures the same idea as classboxes, but it | ||
behaves slightly differently in certain cases (we'll get back to those in a | ||
minute). The differences are not big enough to justify having both refinements | ||
and classboxes, so expect this to be the only way to safely monkey patch in | ||
Ruby in the following years (if the proposal gets accepted of course). | ||
|
||
While this is still only a proposal, Shugo has actually implemented it in Ruby | ||
1.9 *and* provided a patch, so why not install it right away so you can play | ||
with it as we go through the features? | ||
|
||
## Installing | ||
|
||
In order to install the refinements version of Ruby, you need to grab **r29837** | ||
of [trunk][trunk] and apply [the refinements-patch][patch]. If you're using | ||
[rvm][rvm], it's as simple as: | ||
|
||
$ curl -O http://stuff.judofyr.net/refinements.diff | ||
$ rvm install ruby-head-r29837 --patch refinements.diff | ||
$ rvm ruby-head-r29837 | ||
|
||
Or manually: | ||
|
||
$ svn checkout -q -r 29837 http://svn.ruby-lang.org/repos/ruby/trunk ruby-refinements | ||
$ cd ruby-refinements | ||
$ curl http://stuff.judofyr.net/refinements.diff | patch -p1 | ||
$ autoconf | ||
$ ./configure --prefix /usr/local/ruby-refinements | ||
$ make | ||
$ make install | ||
$ export PATH="/usr/local/ruby-refinements/bin:$PATH" | ||
|
||
Now you should be able to run all of the examples given in this article. | ||
|
||
## Refine, don't redefine | ||
|
||
Instead of redefining or defining new methods directly on classes, you'll | ||
create *refinements*: | ||
|
||
module JSONGenerator | ||
refine String do | ||
def to_json; inspect end | ||
end | ||
|
||
refine Fixnum do | ||
def to_json; to_s end | ||
end | ||
|
||
refine Array do | ||
def to_json | ||
# Refinements can see one another, so we can use String#to_json and | ||
# Fixnum#to_json as part of the definition of Array#to_json. | ||
"[" + map { |x| x.to_json } + "]" | ||
end | ||
end | ||
end | ||
{: lang=ruby } | ||
|
||
If you don't do anything other than that, you won't notice anything at all. | ||
However, now you can choose to use this refinement at many different scopes: | ||
|
||
using JSONGenerator # For the whole file | ||
1.to_json | ||
|
||
module Application | ||
using JSONGenerator # For this module and any sub-classes/modules | ||
# E.g. this also applies to Application::Controller | ||
|
||
# It works directly inside the class definition: | ||
2.to_json | ||
|
||
# And inside methods: | ||
def self.hello | ||
3.to_json | ||
end | ||
|
||
class Controllers | ||
using JSONGenerator # For this class and any sub-classes/modules | ||
|
||
def get | ||
using JSONGenerator # For this method only | ||
[1, 2, 3].to_json | ||
end | ||
end | ||
end | ||
{: lang=ruby } | ||
|
||
The great thing about refinements, is that it's technically impossible to | ||
globally leak them. They will always be restricted to the scope you specify, | ||
and there's nothing "above" the file scope. | ||
|
||
That's not *always* true though. Refinements are also enabled in subclasses and | ||
reopened classes, even if they are located in different files. | ||
|
||
class ApplicationController | ||
using JSONGenerator | ||
end | ||
|
||
# Somewhere else: | ||
class ApplicationController | ||
p 123.to_json # Still works | ||
end | ||
|
||
class UsersController < ApplicationController | ||
p 123.to_json # Still works | ||
end | ||
|
||
p 123.to_json # This doesn't work however | ||
{: lang=ruby } | ||
|
||
But here comes the best part: Refinements are also carried on in class_eval, | ||
module_eval and instance_eval: | ||
|
||
module Expectations | ||
refine Object do | ||
def should; ... end | ||
end | ||
end | ||
|
||
def it(msg, &blk) | ||
# Remember that refinements can see one another: | ||
Expectations.module_eval(&blk) | ||
end | ||
|
||
it "should be awesome" do | ||
:refinements.level.should == :awesome | ||
end | ||
{: lang=ruby } | ||
|
||
Holy Schmoly, now we're talking! Even this works as expected: | ||
|
||
class TestScope | ||
using Expectations | ||
attr_reader :msg | ||
|
||
def initialize(msg) | ||
@msg = msg | ||
end | ||
end | ||
|
||
def it(msg, &blk) | ||
TestScope.new(msg).instance_eval(&blk) | ||
end | ||
{: lang=ruby } | ||
|
||
Refinements are also inherited, so Rails 4 could provide this module: | ||
|
||
module ActiveSupport::All | ||
using ActiveSupport::Autoload | ||
using ActiveSupport::Callbacks | ||
# and so on ... | ||
end | ||
{: lang=ruby } | ||
|
||
And because refinements are also enabled in subclasses: | ||
|
||
class ApplicationController < ActionController::Base | ||
using ActiveSupport::All | ||
end | ||
|
||
class ArticlesController < ApplicationController | ||
def index | ||
@articles = Article.where("created_at > ?", 3.days.ago) | ||
end | ||
end | ||
{: lang=ruby } | ||
|
||
You can continue developing **in the exact same way** as before, but now without | ||
leaking anything into the global namespace. | ||
|
||
*(This is the moment where you're proposing to marry Shugo.)* | ||
|
||
## What's the catch? | ||
|
||
There's a few things you need to be aware of. First of all, there might be a | ||
little decrease in performance. Hopefully this will be resolved (or turn out to | ||
be insignificant) in the future. Other issues: | ||
|
||
### #include and #using are completely separated: | ||
|
||
module Rack::Utils | ||
refine Object do | ||
def call; ... end | ||
end | ||
|
||
def escape_html; ... end | ||
end | ||
end | ||
|
||
# I want use both: | ||
module Camping | ||
include Rack::Utils | ||
using Rack::Utils | ||
end | ||
{: lang=ruby } | ||
|
||
Because refinements are lexically scoped, it's also not possible to combine | ||
them with an included hook: | ||
|
||
module Rack::Utils | ||
def self.included(mod) | ||
# Doesn't work as expected: | ||
mod.send(:using, self) | ||
end | ||
end | ||
|
||
module Camping | ||
include Rack::Utils | ||
end | ||
{: lang=ruby } | ||
|
||
You can however use the *used* hook: | ||
|
||
module Rack::Utils | ||
def self.used(mod) | ||
mod.send(:include, self) | ||
end | ||
end | ||
|
||
module Camping | ||
using Rack::Utils | ||
end | ||
{: lang=ruby } | ||
|
||
### Singleton methods in refinements are not included: | ||
|
||
module FixnumExt | ||
# This has no effect: | ||
refine Fixnum do | ||
def self.thing; ... end | ||
end | ||
|
||
# Use this instead: | ||
refine Fixnum.singleton_class do | ||
def thing; ... end | ||
end | ||
end | ||
{: lang=ruby } | ||
|
||
### You can't refine modules: | ||
|
||
module EnumerableExt | ||
# Error: | ||
refine Enumerable do | ||
end | ||
end | ||
{: lang=ruby } | ||
|
||
### Refinements don't have local rebinding | ||
|
||
Another important fact is that, unlike classboxes, refinements don't have | ||
*local rebinding*. Let me show you an example: | ||
|
||
class CharArray | ||
def initialize(str) | ||
@array = str.unpack("C*") # Unpacks to integers | ||
end | ||
|
||
def each(&blk) | ||
@array.each(&blk) | ||
end | ||
|
||
def print_each | ||
each { |chr| p chr } | ||
end | ||
end | ||
|
||
test = CharArray.new("Hello World") | ||
test.print_each # Prints a list of integers (expected) | ||
|
||
# A refinement which overwrites CharArray#each to return one-char strings | ||
# instead of integers: | ||
module CharArrayStr | ||
refine CharArray do | ||
def each | ||
super { |c| yield c.chr } | ||
end | ||
end | ||
end | ||
|
||
using CharArrayStr | ||
test.each { |x| p x } # Prints a list of strings | ||
test.print_each # Prints a list of integers?! | ||
{: lang=ruby } | ||
|
||
At first, it might seem counter-intuitive. Why does the last line prints a list | ||
of integers? Why isn't the refinement enabled in that method? As you might have | ||
guessed, it's because refinements don't have *local rebinding*. This means that | ||
the refinements will *only* apply to the scope they are enabled. The moment you | ||
call a method *outside* of the scope, none of the refinements apply anymore. | ||
|
||
The advantage of this is that you can safely override methods without thinking | ||
about breaking anything else. You simply can't refine code in another scope. | ||
However, there's a huge disadvantage: If there's a "core" method (like #each | ||
above) which is used by several other methods, you can't affect the other | ||
methods. | ||
|
||
Local rebinding might be implemented, but in that case, refinements will be | ||
renamed to classbox (since that's what they are). | ||
|
||
## Current status | ||
|
||
As I've mentioned earlier, there is currently [a patch][patch] available that | ||
builds cleanly on top of r29837. There are some implementation details which | ||
might need to be resolved, but as far as I know, **both matz and ko1 are | ||
positive for merging the patch.** | ||
|
||
## Resources | ||
|
||
* [The slides to Shugo's talk](http://www.slideshare.net/ShugoMaeda/rc2010-refinements) | ||
* [The proposal at ruby-core](http://redmine.ruby-lang.org/issues/show/4085) | ||
|
||
Refinements are very much a work in progress, so if you have any more details | ||
or questions about how they work, feel free to contact me on | ||
<[email protected]> so I can keep this article up to date. | ||
|
||
(Thanks to Shugo Maeda for explaining in detail how refinements work, and [Rune | ||
Botten][runeb] and [Peter Aronoff][telemachus] for reading drafts of this.) | ||
|
||
[rc-talk]: http://www.rubyconf.org/presentations/51 | ||
[classbox]: http://scg.unibe.ch/research/classboxes | ||
[classbox-paper]: http://scg.unibe.ch/archive/papers/Berg03aClassboxes.pdf | ||
[trunk]: http://svn.ruby-lang.org/repos/ruby/trunk | ||
[patch]: http://stuff.judofyr.net/refinements.diff | ||
[rvm]: http://rvm.beginrescueend.com/ | ||
[telemachus]: http://ithaca.arpinum.org/ | ||
[runeb]: http://runerb.com/ | ||
|