Permalink
Browse files

Add support for assignment &X['one'] = 2

  • Loading branch information...
1 parent 4d91ba8 commit abf7c7c1c2f12d088ba7550c7ace8abb73e06528 @ConradIrwin ConradIrwin committed Nov 18, 2010
Showing with 105 additions and 2 deletions.
  1. +18 −1 README.markdown
  2. +47 −1 lib/ampex.rb
  3. +40 −0 spec/ampex_spec.rb
View
@@ -1,5 +1,8 @@
The Ampex (`&X`) library provides a Metavariable X that can be used in conjunction with the unary ampersand to create anonymous blocks in a slightly more readable way than the default. It was inspired by the clever `Symbol#to_proc` method which handles the most common case very elegantly, and discussion with Sam Stokes who created an earlier version.
+Usage
+-----
+
At its simplest, `&X` can be used as a drop-in replacement for `Symbol#to_proc`:
[1,2,3].map &X.to_s
@@ -21,12 +24,18 @@ And, as everything in ruby is a method, create readable expressions without the
["a", "b", "c"].map &(X * 2)
# => ["aa", "bb", "cc"]
+ [{}].each &X[1] = 2
+ # => [{1 => 2}]
+
As an added bonus, the effect is transitive — you can chain method calls:
[1, 2, 3].map &X.to_f.to_s
# => ["1.0", "2.0", "3.0"]
-There are two things to watch out for:
+Gotchas
+-------
+
+There are a few things to watch out for:
Firstly, `&X` can only appear on the left:
@@ -52,6 +61,14 @@ Secondly, other arguments or operands will only be evaluated once, and not every
[1, 2].map{ |x| x + (i += 1) }
# => [2, 4]
+Bugs
+----
+
+If you create an assigning callable (e.g. `X[a] = b`, `X.a = b` ) without an immediate preceding `&`, then `b.class#to_proc` will return the assigning callable the first time, and only the first time, you call it. If you want to get access to an assigning callable that you've defined using `&X`, you must do: `lambda &X[a] = b` instead.
+
+
+Epilogue
+--------
For bug-fixes or enhancements, please contact the author: Conrad Irwin <conrad.irwin@gmail.com>
View
@@ -11,7 +11,9 @@ def initialize(parent=nil, &block)
end
def method_missing(name, *args, &block)
- Metavariable.new(self) { |x| x.send(name, *args, &block) }
+ mv = Metavariable.new(self) { |x| x.send(name, *args, &block) }
+ Metavariable.temporarily_monkeypatch(args.last.class, mv) if name.to_s =~ /=$/
+ mv
end
def to_proc
@@ -24,6 +26,50 @@ def to_proc
end
end
end
+
+ private
+
+ # In order to support assignment via &X (expressions of the form &X['one'] = 2),
+ # we need to add 2.to_proc (because assignment in ruby always returns the operand)
+ #
+ # Luckily, we only need to do this for a very short time.
+ #
+ # When given an expression such as:
+ #
+ # ary.map(&X[args(a)] = :two)
+ #
+ # the order of execution is:
+ # args(a)
+ # X[_] = :two \_ need to patch here
+ # :two.to_proc _/ and un-patch here
+ # ary.map &_
+ #
+ # There is a risk if someone uses an amp-less X, and assigns something with to_proc
+ # (most likely a symbol), and then uses .map(&:to_i), as &:to_i will return the
+ # behaviour of their metavariable.
+ #
+ # There are other things that might notice us doing this, if people are listening
+ # on various method_added hooks, or have overridden class_eval, etc. But I'm not
+ # too worried.
+ #
+ def self.temporarily_monkeypatch(klass, mv)
+ klass.send :class_variable_set, :'@@metavariable', mv
+ klass.class_eval do
+
+ alias_method(:to_proc_without_metavariable, :to_proc) rescue nil
+ def to_proc
+ self.class.class_eval do
+
+ undef to_proc
+ alias_method(:to_proc, :to_proc_without_metavariable) rescue nil
+ undef to_proc_without_metavariable rescue nil
+
+ # Remove the metavariable from the class and return its proc
+ remove_class_variable(:'@@metavariable').to_proc
+ end
+ end
+ end
+ end
end
X = Metavariable.new
View
@@ -25,6 +25,26 @@
[1, 2, 3].map(&X.to_f.to_s).should == ["1.0", "2.0", "3.0"]
end
+ it "should allow assignment" do
+ [{}].each(&X['a'] = 1).should == [{'a' => 1}]
+ end
+
+ it "should not leak #to_proc" do
+ [{}].map(&X['a'] = 1).first.should_not respond_to :to_proc
+ end
+
+ it "should not be possible to intercept #to_proc" do
+ b = Object.new
+ def intercept(b)
+ b.respond_to? :to_proc
+ end
+ [{}].each(&X[intercept(b)] = b).should == [{false => b}]
+ end
+
+ it "should preserve existing #to_proc" do
+ [{}].each(&X[:to_a] = :to_a).map(&:to_a).should == [[[:to_a, :to_a]]]
+ end
+
it "should only evaluate arguments once" do
@counted = 0
def count
@@ -35,4 +55,24 @@ def count
@counted.should == 1
end
+ it "shouldn't, but does, make a mess of split assignment" do
+ def assigner(key, value); X[key] = value; end
+ twoer = assigner(1, 2)
+ [{}].each(&twoer).should == [{1 => 2}]
+ lambda { [{}, {}].each(&twoer).should == 1 }.should raise_error
+
+ mehier = assigner(1, :inspect)
+ [{}].map(&:inspect).should == [:inspect]
+ end
+
+ it "should allow you to create lambdas" do
+ def assigner(key, value); lambda &X[key] = value; end
+ twoer = assigner(1, 2)
+ [{}].each(&twoer).should == [{1 => 2}]
+ [{}, {}].each(&twoer).should == [{1 => 2}, {1 => 2}]
+
+ mehier = assigner(1, :inspect)
+ [{}].map(&:inspect).should == ["{}"]
+ end
+
end

0 comments on commit abf7c7c

Please sign in to comment.