Ways to change the behavior of a block

B

Brian Guthrie

I'd like to be able to change the behavior of a call to a block (I'm
working on a design-by-contract system and would like to be able to
check the block's signature, filtering each call through some contract
check) and I was wondering if anyone had any advice. It's easy enough
to override Proc#call, but doing so doesn't appear to affect calls to
implicit blocks with using the yield keyword. Is it even possible to
do this?

Cheers,

Brian Guthrie
 
R

Robert Klemme

I'd like to be able to change the behavior of a call to a block (I'm
working on a design-by-contract system and would like to be able to
check the block's signature, filtering each call through some contract
check) and I was wondering if anyone had any advice. It's easy enough
to override Proc#call, but doing so doesn't appear to affect calls to
implicit blocks with using the yield keyword. Is it even possible to
do this?

If I understand you properly you want to be checking the argument list
in the *calling* method.

irb(main):003:0> def f(&b)
irb(main):004:1> raise ArgumentError unless b.arity == 4
irb(main):005:1> b[0,1,2,3]
irb(main):006:1> end
=> nil
irb(main):007:0> f {|a,b| p a,b}
ArgumentError: ArgumentError
from (irb):4:in `f'
from (irb):7
from :0
irb(main):008:0> f {|a,b,c,d| p a,b}
0
1
=> nil

Kind regards

robert
 
B

Brian Guthrie

I'd like to be able to change the behavior of a call to a block (I'm
working on a design-by-contract system and would like to be able to
check the block's signature, filtering each call through some contract
check) and I was wondering if anyone had any advice. It's easy enough
to override Proc#call, but doing so doesn't appear to affect calls to
implicit blocks with using the yield keyword. Is it even possible to
do this?

If I understand you properly you want to be checking the argument list
in the *calling* method.

irb(main):003:0> def f(&b)
irb(main):004:1> raise ArgumentError unless b.arity == 4
irb(main):005:1> b[0,1,2,3]
irb(main):006:1> end
=> nil
irb(main):007:0> f {|a,b| p a,b}
ArgumentError: ArgumentError
from (irb):4:in `f'
from (irb):7
from :0
irb(main):008:0> f {|a,b,c,d| p a,b}
0
1
=> nil

Kind regards

robert

It's a bit more complicated than that, but that's the idea. If you're
curious, the library is called Handshake (handshake.rubyforge.org;
haven't made an announcement as it's not quite mature yet). It
supports (among other things) argument contracts of the form:

contract String => Integer
def to_i ...

contract "foo" => 1..3
def foo ...

contract any?( String, hash_of?(Symbol, Fixnum) ) => String
def accepts_string_or_hash ...

The goal is to extend it to support block contracts:

contract [ String, Block(String => Integer) ] => Integer

The Handshake library surrounds an object that includes the
appropriate module with a proxy object and checks everything that
passes across that barrier. That means that I can easily manipulate
any incoming Proc objects, extending the instance so that Proc#call is
required to check an argument list. The problem is that changing the
behavior of Proc#call does _not_ affect the behavior of yield:

class Proc
def weird(*args)
puts "weird call!" if args.length > 0 && args[0] == "weird"
orig_call(*args)
end
alias :eek:rig_call :call
alias :call :weird
end

def weird_call(str, &block)
block.call(str)
end

def weird_yield(str)
yield(str)
end

In IRB:
weird call!
=> true=> true

Cheers,

Brian
 
G

Gary Wright

The Handshake library surrounds an object that includes the
appropriate module with a proxy object and checks everything that
passes across that barrier. That means that I can easily manipulate
any incoming Proc objects, extending the instance so that Proc#call is
required to check an argument list. The problem is that changing the
behavior of Proc#call does _not_ affect the behavior of yield:

The 'yield' mechanism is not a method call and so you won't be able
to intercept it by redefining methods. You would have to hack on the
Ruby interpreter itself.

I'll admit to be pretty leary of something that redefines Proc#call
at the class level (vs. via singleton methods on particular procs).

Gary Wright
 
B

Brian Guthrie

On 30.04.2007 07:31, Brian Guthrie wrote:
I'd like to be able to change the behavior of a call to a block (I'm
working on a design-by-contract system and would like to be able to
check the block's signature, filtering each call through some contract
check) and I was wondering if anyone had any advice. It's easy enough
to override Proc#call, but doing so doesn't appear to affect calls to
implicit blocks with using the yield keyword. Is it even possible to
do this?

If I understand you properly you want to be checking the argument list
in the *calling* method.

irb(main):003:0> def f(&b)
irb(main):004:1> raise ArgumentError unless b.arity == 4
irb(main):005:1> b[0,1,2,3]
irb(main):006:1> end
=> nil
irb(main):007:0> f {|a,b| p a,b}
ArgumentError: ArgumentError
from (irb):4:in `f'
from (irb):7
from :0
irb(main):008:0> f {|a,b,c,d| p a,b}
0
1
=> nil

Kind regards

robert

It's a bit more complicated than that, but that's the idea. If you're
curious, the library is called Handshake (handshake.rubyforge.org;
haven't made an announcement as it's not quite mature yet). It
supports (among other things) argument contracts of the form:

contract String => Integer
def to_i ...

contract "foo" => 1..3
def foo ...

contract any?( String, hash_of?(Symbol, Fixnum) ) => String
def accepts_string_or_hash ...

The goal is to extend it to support block contracts:

contract [ String, Block(String => Integer) ] => Integer

The Handshake library surrounds an object that includes the
appropriate module with a proxy object and checks everything that
passes across that barrier. That means that I can easily manipulate
any incoming Proc objects, extending the instance so that Proc#call is
required to check an argument list. The problem is that changing the
behavior of Proc#call does _not_ affect the behavior of yield:

Then just create another block that will invoke the original and do the
checks.

def f(&b)
bb = lambda {|*a|
raise ArgumentError unless a.size == 4
result = b[*a[0...4]]
raise "Whatever" unless Array === result
result
}
other_method(&bb)
end

Kind regards

robert

That would work, I think. I'll do that. Thanks for the advice.
 
B

Brian Guthrie

The 'yield' mechanism is not a method call and so you won't be able
to intercept it by redefining methods. You would have to hack on the
Ruby interpreter itself.

I'll admit to be pretty leary of something that redefines Proc#call
at the class level (vs. via singleton methods on particular procs).

Gary Wright

Indeed. The example I gave did that for simplicity's sake but the
library would only modify the incoming objects.

It's the sort of library that shouldn't necessarily be used in
production code anyway, though, due to the performance hit you take
from having to run each and every single method call through a filter.

Brian
 
G

Gary Wright

It's the sort of library that shouldn't necessarily be used in
production code anyway, though, due to the performance hit you take
from having to run each and every single method call through a filter.

Makes sense. Reminds me of Eiffel's ability to monitor pre/post
conditions during development but to turn off those checks in
production.

One of the things that I thought was really nice about Eiffel's
pre-condition checks was that any pre-condition exceptions were
raised in the caller's context, which is conceptually where the
error exists. I think that would be difficult to do in Ruby without
support in the runtime.

Gary Wright
 
B

Brian Guthrie

Makes sense. Reminds me of Eiffel's ability to monitor pre/post
conditions during development but to turn off those checks in
production.

One of the things that I thought was really nice about Eiffel's
pre-condition checks was that any pre-condition exceptions were
raised in the caller's context, which is conceptually where the
error exists. I think that would be difficult to do in Ruby without
support in the runtime.

Gary Wright

You can actually get a certain amount of the way there. I'm currently
supporting a number of different checks:

- invariants
- pre/post conditions
- method argument checks

There are a variety of different failure conditions for these. If a
post-condition check fails then the blame lies with the method in
question. If a precondition check fails then the caller is at fault.
Invariant checks are very difficult to pinpoint: if an invariant
doesn't hold before a method is called it's very hard to figure out
why.

I'm currently doing my best to raise in the context of the method
around which the contract is placed but it's very difficult to raise
in the right place and to assign blame correctly. Raising in the
context of the caller may be impossible without, as you suggest,
support in the runtime. My understanding is that libraries exist for
deriving a useful call stack but I haven't explored them yet.

There's a gem if you're curious but the documentation isn't as good as
I'd like yet, and there are a few bugs. You may also want to check
out Florian Gro=DF's ruby-contract.

Brian Guthrie
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
474,241
Messages
2,571,219
Members
47,850
Latest member
StewartTha

Latest Threads

Top