Skip to content
This repository has been archived by the owner on Dec 5, 2023. It is now read-only.

Commit

Permalink
Remove support for assignment.
Browse files Browse the repository at this point in the history
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
ConradIrwin committed May 20, 2012
1 parent 954615c commit ab1646f
Show file tree
Hide file tree
Showing 4 changed files with 17 additions and 156 deletions.
16 changes: 8 additions & 8 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
--------

Expand All @@ -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
--------

Expand Down
2 changes: 1 addition & 1 deletion ampex.gemspec
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
82 changes: 2 additions & 80 deletions lib/ampex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
73 changes: 6 additions & 67 deletions spec/ampex_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down

0 comments on commit ab1646f

Please sign in to comment.