Skip to content

Commit

Permalink
Add #respond_to? tests and fix behaviour
Browse files Browse the repository at this point in the history
The previous implementation was completely wrong, which I didn’t realise
until I tried to add tests around it. This commit adds the tests and
simultaneously fixes the code to actually work.

A generic implementation of #respond_to_missing? can’t work[1] because
answering the question requires knowledge of #and_then’s control flow;
for example, Optional only sends the message to the underlying value if
it’s not nil, so #respond_to_missing? should return true if the value is
nil and ask if it responds to the message otherwise.

We have no way of knowing what #and_then’s control flow will look like
in general, so we just have to lovingly recreate it in #respond_to_missing?
for each monad, which is annoying but necessary.

[1] In fact, it *could* work by simply sending the message and reporting
whether a NameError is raised, but that would defeat the entire purpose
of #respond_to?, namely to tell you whether a message can be sent
without actually sending it.
  • Loading branch information
tomstuart committed Feb 20, 2016
1 parent aebc018 commit 3e0cdd7
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 12 deletions.
4 changes: 4 additions & 0 deletions lib/monads/eventually.rb
Expand Up @@ -24,6 +24,10 @@ def and_then(&block)
end
end

def respond_to_missing?(method_name, include_private = false)
super || run { |value| value.respond_to?(method_name, include_private) }
end

def self.from_value(value)
Eventually.new do |success|
success.call(value)
Expand Down
4 changes: 4 additions & 0 deletions lib/monads/many.rb
Expand Up @@ -16,6 +16,10 @@ def and_then(&block)
Many.new(values.map(&block).flat_map(&:values))
end

def respond_to_missing?(method_name, include_private = false)
super || values.all? { |value| value.respond_to?(method_name, include_private) }
end

def self.from_value(value)
Many.new([value])
end
Expand Down
6 changes: 0 additions & 6 deletions lib/monads/monad.rb
Expand Up @@ -12,12 +12,6 @@ def method_missing(*args, &block)
end
end

def respond_to_missing?(method_name, include_private = false)
within do |value|
value.respond_to?(method_name, include_private)
end || super
end

private

def ensure_monadic_result(&block)
Expand Down
4 changes: 4 additions & 0 deletions lib/monads/optional.rb
Expand Up @@ -20,6 +20,10 @@ def and_then(&block)
end
end

def respond_to_missing?(method_name, include_private = false)
super || value.nil? || value.respond_to?(method_name, include_private)
end

def self.from_value(value)
Optional.new(value)
end
Expand Down
22 changes: 20 additions & 2 deletions spec/monads/eventually_spec.rb
Expand Up @@ -102,8 +102,6 @@ module Monads
it 'forwards any unrecognised message to the block’s value' do
expect(value).to receive(:challenge)
Eventually.new { |success| success.call(value) }.challenge.run {}
expect { Eventually.new { |success| success.call(value) }.method(:challenge) }.not_to raise_error
expect(Eventually.new { |success| success.call(value) }).to respond_to(:challenge)
end

it 'returns the message’s result wrapped in an Eventually' do
Expand All @@ -112,6 +110,26 @@ module Monads
expect(@result).to eq response
end

context 'when the value responds to the message' do
it 'reports that the Eventually responds to the message' do
expect(Eventually.new { |success| success.call(value) }).to respond_to(:challenge)
end

it 'allows a Method object to be retrieved' do
expect(Eventually.new { |success| success.call(value) }.method(:challenge)).to be_a(Method)
end
end

context 'when the value doesn’t respond to the message' do
it 'reports that the Eventually doesn’t respond to the message' do
expect(Eventually.new { |success| success.call(double) }).not_to respond_to(:challenge)
end

it 'doesn’t allow a Method object to be retrieved' do
expect { Eventually.new { |success| success.call(double) }.method(:challenge) }.to raise_error(NameError)
end
end

context 'when value is Enumerable' do
let(:value) { [1, 2, 3] }

Expand Down
24 changes: 22 additions & 2 deletions spec/monads/many_spec.rb
Expand Up @@ -87,14 +87,34 @@ module Monads
expect(value).to receive(:challenge)
end
many.challenge
expect { many.method(:challenge) }.not_to raise_error
expect(many).to respond_to(:challenge)
end

it 'returns the messages’ results wrapped in a Many' do
expect(many.challenge.values).to eq responses
end

context 'when all of the values respond to the message' do
it 'reports that the Many responds to the message' do
expect(many).to respond_to(:challenge)
end

it 'allows a Method object to be retrieved' do
expect(many.method(:challenge)).to be_a(Method)
end
end

context 'when any of the values don’t respond to the message' do
let(:many) { Many.new(values + [double]) }

it 'reports that the Many doesn’t respond to the message' do
expect(many).not_to respond_to(:challenge)
end

it 'doesn’t allow a Method object to be retrieved' do
expect { many.method(:challenge) }.to raise_error(NameError)
end
end

context 'when values are Enumerable' do
let(:values) { [[1, 2], [3, 5]] }

Expand Down
24 changes: 22 additions & 2 deletions spec/monads/optional_spec.rb
Expand Up @@ -89,14 +89,34 @@ module Monads
it 'forwards any unrecognised message to the value' do
expect(value).to receive(:challenge)
optional.challenge
expect { optional.method(:challenge) }.not_to raise_error
expect(optional).to respond_to(:challenge)
end

it 'returns the message’s result wrapped in an Optional' do
expect(optional.challenge.value).to eq response
end

context 'when the value responds to the message' do
it 'reports that the Optional responds to the message' do
expect(optional).to respond_to(:challenge)
end

it 'allows a Method object to be retrieved' do
expect(optional.method(:challenge)).to be_a(Method)
end
end

context 'when the value doesn’t respond to the message' do
let(:optional) { Optional.new(double) }

it 'reports that the Optional doesn’t respond to the message' do
expect(optional).not_to respond_to(:challenge)
end

it 'doesn’t allow a Method object to be retrieved' do
expect { optional.method(:challenge) }.to raise_error(NameError)
end
end

context 'when value is Enumerable' do
let(:value) { [1, 2, 3] }

Expand Down

0 comments on commit 3e0cdd7

Please sign in to comment.