Permalink
Browse files

Remove support for assignment.

While it's an excellent piece of show-off thread safe code; it's not
actually useful, and contains some nasty edge-cases that are not
solvable.
  • Loading branch information...
1 parent 954615c commit ab1646f7edbb2d1f5b2fa51e013433d956b9415e @ConradIrwin ConradIrwin committed May 20, 2012
Showing with 17 additions and 156 deletions.
  1. +8 −8 README.markdown
  2. +1 −1 ampex.gemspec
  3. +2 −80 lib/ampex.rb
  4. +6 −67 spec/ampex_spec.rb
View
@@ -29,9 +29,6 @@ As everything in Ruby is a method call, you can create readable expressions with
["a", "b", "c"].map &(X * 2)
# => ["aa", "bb", "cc"]
- [{}].each &X[1] = 2
- # => [{1 => 2}]
-
You can use this in any place a block is expected, for example to create a lambda:
normalizer = lambda &X.to_s.downcase
@@ -67,11 +64,6 @@ Secondly, other arguments or operands will only be evaluated once, and not every
[1, 2].map{ |x| x + (i += 1) }
# => [2, 4]
-Bugs
-----
-
-In normal usage there are no known bugs. That said, if you accidentally miss the `&` from in front of the `X`, in an expression that ends in an assignment (e.g. `X.formatter = :inspect`); then the `#to_proc` method of the object assigned will respond with the expression generated by that `X` the next time you call it from anywhere else in the same thread.
-
Epilogue
--------
@@ -83,6 +75,14 @@ For an up-to-date version, try <https://github.com/rapportive-oss/ampex>
This library is copyrighted under the MIT license, see LICENSE.MIT for details.
+
+Backwards compatibility breakages
+---------------------------------
+
+Between version 1.2.1 and version 2.0.0, the support for assignment operations was removed from
+ampex. These had a very non-obvious implementation, and it was impossible to support
+assigning of falsey values; and did not work on rubinius.
+
See also
--------
View
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = "ampex"
- s.version = "1.2.1"
+ s.version = "2.0.0"
s.platform = Gem::Platform::RUBY
s.author = "Conrad Irwin"
s.email = "conrad.irwin@gmail.com"
View
@@ -14,10 +14,6 @@
end
class Metavariable < superclass
- # Take a local copy of these as constant lookup is destroyed by BasicObject.
- Metavariable = self
- Thread = ::Thread
-
# When you pass an argument with & in ruby, you're actually calling #to_proc
# on the object. So it's Symbol#to_proc that makes the &:to_s trick work,
# and Metavariable#to_proc that makes &X work.
@@ -39,89 +35,15 @@ def initialize(&block)
# }
#
def method_missing(name, *args, &block)
- mv = Metavariable.new { |x| @to_proc.call(x).__send__(name, *args, &block) }
- Metavariable.temporarily_monkeypatch(args.last, :to_proc) { mv.to_proc } if name.to_s =~ /[^!=<>]=$/
- mv
+ raise ::NotImplementedError, "(&X = 'foo') is unsupported in ampex > 2.0.0" if name.to_s =~ /[^!=<>]=$/
+ ::Metavariable.new { |x| @to_proc.call(x).__send__(name, *args, &block) }
end
# BlankSlate and BasicObject have different sets of methods that you don't want.
# let's remove them all.
instance_methods.each do |method|
undef_method method unless %w(method_missing to_proc __send__ __id__).include? method.to_s
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 &_
- #
- # We go to some lengths to ensure that, providing the & and the X are adjacent,
- # it's not possible to get different behaviour in the rest of the program; despite
- # the temporary mutation of potentially global state.
- #
- # We can't really do anything if the & has been split from the X, consider:
- #
- # assigner = (X[0] = :to_i)
- # assigner == :to_i
- # # => true
- # [1,2,3].map(&:to_i)
- # # => NoMethodError: undefined method `[]=' for 1:Fixnum
- #
- # Just strongly encourage use of:
- # assigner = lambda &X = :to_i
- # assigner == :to_i
- # # => false
- # [1,2,3].map(&:to_i)
- # # => [1,2,3]
- #
- def self.temporarily_monkeypatch(instance, method_name, &block)
-
- Thread.exclusive do
- @monkey_patch_count = @monkey_patch_count ? @monkey_patch_count + 1 : 0
- stashed_method_name = :"#{method_name}_without_metavariable_#{@monkey_patch_count}"
- thread = Thread.current
-
- # Try to get a handle on the object's singleton class, but fall back to using
- # its actual class where that is not possible (i.e. for numbers and symbols)
- klass = (class << instance; self; end) rescue instance.class
- klass.class_eval do
-
- alias_method(stashed_method_name, method_name) rescue nil
- define_method(method_name) do
-
- todo = block
-
- Thread.exclusive do
- if self.equal?(instance) && thread.equal?(Thread.current)
-
- klass.class_eval do
- undef_method(method_name)
- alias_method(method_name, stashed_method_name) rescue nil
- undef_method(stashed_method_name) rescue nil
- end
-
- else
- todo = method(stashed_method_name)
- end
- end
-
- todo.call
- end
- end
- end
- end
end
X = Metavariable.new
View
@@ -25,52 +25,17 @@
[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}]
+ it "should not allow assignment" do
+ lambda{
+ [{}].each(&X['a'] = 1).should == [{'a' => 1}]
+ }.should raise_error
end
- it "should not leak #to_proc" do
- [{}].map(&X['a'] = 1).first.should_not respond_to :to_proc
- end
-
- it "should not leak #to_proc on comparison" do
+ it "should support ==" do
[:a, :b, :c].map(&X == :to_i)
[1,2,3].map(&:to_i).should == [1,2,3]
end
- it "should not be possible to intercept #to_proc" do
- b = Object.new
- def intercept(b)
- b.to_proc
- rescue NoMethodError => e
- e.class
- end
- [{}].each(&X[intercept(b)] = b).should == [{NoMethodError => b}]
- end
-
- it "should not be possible to intercept #to_proc in an interrupting thread" do
- X[0] = :inspect
- b = []
- Thread.new { b << [1,2,3].map(&:inspect) }.join
- b.should == [["1","2", "3"]]
- [].map(&:inspect)
- 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 preserve existing #to_proc in an object's singleton class" do
- a = Object.new
- class << a
- def to_proc; lambda { |x| 3 }; end
- end
-
- [1].map(&a).should == [3]
- [{1 => 2}].each(&X[1] = 3).should == [{1 => 3}]
- [1].map(&a).should == [3]
- end
-
it "should only evaluate arguments once" do
@counted = 0
def count
@@ -81,35 +46,9 @@ 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
-
- it "should not be perturbed by an ampless X" do
- X[0] = 1
- [{1 => 2}].each(&X[1] = 3).should == [{1 => 3}]
- [].map(&1)
- end
-
it "should work in the face of an overridden #send" do
class A
- def send; "Dear Aunty Mabel, I'm writing to you"; end
+ def send; "Dear Aunty Mabel, I'm writing to you..."; end
def sign_off; "Yours relatedly, Cousin Sybil"; end
end

0 comments on commit ab1646f

Please sign in to comment.