Polymorphic block parameters -- an idea for Ruby 2.0

T

Trans

I have been working on a project where a particularly important
parameter can either be a hash (or hash-like object) or a proc. With
the later it is very nice to be able to use a block. But having to
accept both types of object makes it akward b/c of the distinction Ruby
makes between these kinds of parameters, i.e. arg vs. &arg. While this
has occured to me before with regards to default values (becuase blocks
parameters can not have default values) this issue makes the situation
particularly stark. Ruby is working against itself here. While Ruby
promotes duck-typing, in this particular case Ruby prevents it. The
prevention arises, it seems, from the a certain concept of yeild and
block_given? --that which makes a block an option on any method
wahtesoever without regard to the methods definition. While seemingly
convenient, this actually creates constraints that prove to be less
than convenient. After much consideration,I think Ruby is probably
being a little too magical for it's own good here.

Just to be very clear, here is a simple example to demonstrate what I
mean about the lack of duck-typing with the block parameter. One can
exrapolate other exmaples including the converse situation of accepting
the data but not the block.

def foo( &b )
b.call
end

a = "Quack"
def a.call ; self ; end

foo { "Quack" } #=> "Quack"
foo "Hello" #=> error

So given this, I wonder if it might not be better to get rid of these
special block parameters? Why not have a normal parameter, the last one
in the list, pick up the block? In other words what if

foo { "Quack" }

were ssentially the same as doing:

foo( lambda { "Quack" } )

The difference being just one of convenience. Then simply using

def foo( b )
b.call
end

would work fine. No only does it provide for the polymorphism. But it
aslo allows control over when a block parameter is required or can be
omitted by providing a default (eg. 'foo(b=nil)' or 'foo(b=proc{...})'
). Moreover, it simplifies the syntax a touch too --that's always nice.

Of course we must also ask what happens to #yield and #block_given?. At
first I thought they'd have to be deprecated. But I later realized not.
They could still work just as before. The only new requirement is that
a parameter must always be provided to capture the block. This
requirement on the method interface is more self documenting anyway and
in my experience using a parameter proves to be more useful and
actually faster besides.

Clearly this change is a Ruby 2.0 level change but I think it is worthy
of consideration. What do you think?

T.
 
R

Robert Klemme

Trans said:
I have been working on a project where a particularly important
parameter can either be a hash (or hash-like object) or a proc. With
the later it is very nice to be able to use a block. But having to
accept both types of object makes it akward b/c of the distinction Ruby
makes between these kinds of parameters, i.e. arg vs. &arg. While this
has occured to me before with regards to default values (becuase blocks
parameters can not have default values) this issue makes the situation
particularly stark. Ruby is working against itself here. While Ruby
promotes duck-typing, in this particular case Ruby prevents it. The
prevention arises, it seems, from the a certain concept of yeild and
block_given? --that which makes a block an option on any method
wahtesoever without regard to the methods definition. While seemingly
convenient, this actually creates constraints that prove to be less
than convenient. After much consideration,I think Ruby is probably
being a little too magical for it's own good here.

I fail to see the problem.
def foo(ha = {}, &b)
(b || ha)[:what_ever]
end => nil
foo :what_ever => "123" => "123"
foo { |a| "678" }
=> "678"
Just to be very clear, here is a simple example to demonstrate what I
mean about the lack of duck-typing with the block parameter. One can
exrapolate other exmaples including the converse situation of accepting
the data but not the block.

def foo( &b )
b.call
end

a = "Quack"
def a.call ; self ; end

foo { "Quack" } #=> "Quack"
foo "Hello" #=> error

"Hello" is not a hash like parameter. You don't use a.call in your example.
def foo(s = nil, &b)
b ? b[] : s
end => nil
foo "hello" => "hello"
foo { "hello" }
=> "hello"
def foo(s = nil, &b)
s || b[]
end => nil
foo "hello" => "hello"
foo { "hello" }
=> "hello"

Note that this is really a silly (as opposed to "simple") example
because it does not make sense to provide a block that returns a
constant value. Also usually a block accepts a parameter so it can do
something with it.
So given this, I wonder if it might not be better to get rid of these
special block parameters? Why not have a normal parameter, the last one
in the list, pick up the block? In other words what if

foo { "Quack" }

were ssentially the same as doing:

foo( lambda { "Quack" } )

First, that'll break a lot of code. Second, you loose easy access to
the block. Third, you cannot provide a parameter *and* a block at the
same time any more - at least not without having two method parameters
which defies the purpose of your suggestion IMHO.
The difference being just one of convenience. Then simply using

def foo( b )
b.call
end

would work fine. No only does it provide for the polymorphism. But it
aslo allows control over when a block parameter is required or can be
omitted by providing a default (eg. 'foo(b=nil)' or 'foo(b=proc{...})'
). Moreover, it simplifies the syntax a touch too --that's always nice.

You forget that you can use normal parameters as they are but need to
invoke "call" or "[]" on the block. You have to distinguish them anyway.
Of course we must also ask what happens to #yield and #block_given?. At
first I thought they'd have to be deprecated. But I later realized not.
They could still work just as before. The only new requirement is that
a parameter must always be provided to capture the block. This
requirement on the method interface is more self documenting anyway and
in my experience using a parameter proves to be more useful and
actually faster besides.

Clearly this change is a Ruby 2.0 level change but I think it is worthy
of consideration. What do you think?

I'm clearly objecting it as I see too many disadvantages vs. too few
advantages.

Kind regards

robert
 
D

Daniel Schierbeck

Trans said:
... long post ...

So what you're basically suggesting is making procs 1st class objects?

foo({|bar| ... })

or that blocks should just be appended to the argument list?

def foo(a, b, block); end
foo:)a, :b){ ... }

Personally, I like how it work now -- oftentimes you need a variable
number of arguments and a block, and the &block syntax is very easy to
use. Using hashes and procs interchangeably isn't hard at the moment,
either:

def foo(hsh = {}, &blk)
(blk || hsh)["bar"]
end

You could even see procs and hashes as being the same:

class Proc
def to_hash
Hash.new{|key| hsh[key] = call(key)}
end
end

class Hash
def to_proc
proc{|key| self[key]}
end
end

hsh = {:a => 'foo', :b => 'bar', :c => 'baz'}
[:a, :b, :c].collect(&hsh) #=> ["foo", "bar", "baz"]


Cheers,
Daniel
 
T

Trans

Robert said:
Trans wrote:
I fail to see the problem.
def foo(ha = {}, &b)
(b || ha)[:what_ever]
end => nil
foo :what_ever => "123" => "123"
foo { |a| "678" }
=> "678"

"Problem" is relative. I can write progams in Assemler too, but I
choode to use Ruby. Right? Here you actually demonstrate what I mean by
showing what one has to do to get around the "problem". THis might not
seem an issue, but consider the use case where your end user if a
developer and you're asking him to define some methods to inteface to
your library. It is not behoving to ease of use, elegance to have to
require this kind of interface and subcode on every such method.
Morover your's using a conincidental similarity between a Hash and Proc
of the method []. THough my particular case generally needs the
polymorphism between Hash and Proc, this is but a specific case. My
point is too the general.
"Hello" is not a hash like parameter. You don't use a.call in your example.

As I said, my point is too the general case, and I used the simplest
example to that effect I could think of off the top of my head. It has
nothing to do with Hashes per se. And I think that is rather obvious.
def foo(s = nil, &b)
b ? b[] : s
end => nil
foo "hello" => "hello"
foo { "hello" }
=> "hello"
def foo(s = nil, &b)
s || b[]
end => nil
foo "hello" => "hello"
foo { "hello" }
=> "hello"

Note that this is really a silly (as opposed to "simple") example
because it does not make sense to provide a block that returns a
constant value. Also usually a block accepts a parameter so it can do
something with it.

Actually this comment is silly. "Silly" examples often make the most
obvious demonstrations. Do you really think people use #foo in their
programs? ;-)
First, that'll break a lot of code.

Which is why I say it is definitely a 2.0 idea.
Second, you loose easy access to the block.

How is that? You have access via the parameter. Morevoer yield can
still function, so I do see how that that is the case. Hmm... I'm
starting to think I've been misunderstood.
Third, you cannot provide a parameter *and* a block at the
same time any more - at least not without having two method parameters
which defies the purpose of your suggestion IMHO.

Uh? That doesn't make any sense. One parameter can take any object,
including a proc provided via a block. No need for two parameters
--which is exactly what I'm after. Okay I'm pretty sure I'm being
misunderstood now. Tell you waht, I append another post after this one
with a bunch of examples of what I am proposing.
You forget that you can use normal parameters as they are but need to
invoke "call" or "[]" on the block. You have to distinguish them anyway.

Not so. Polymorphism/duck-typing allows other objects to respond to the
same methods. Hence I don't have to distinguish them anyway. Indeed,
that's the whole point.
I'm clearly objecting it as I see too many disadvantages vs. too few
advantages.

It would appear you have elucidated only one disadvantage, that of
backward compatibily. That is indeed someting to be heavily weighed,
but I fail to see the "many" too which you refer. Please give it some
more thought. And if I haven't explained myself well enough, perhaps my
next post will help.

Thanks,
T.
 
P

Phil Tomson

So what you're basically suggesting is making procs 1st class objects?

procs are 1st class objects (of class Proc)...
foo({|bar| ... })

or that blocks should just be appended to the argument list?

def foo(a, b, block); end
foo:)a, :b){ ... }

Yes, I think he's asking for an implicit block parameter on all calls.
Personally, I like how it work now -- oftentimes you need a variable
number of arguments and a block, and the &block syntax is very easy to
use. Using hashes and procs interchangeably isn't hard at the moment,
either:

def foo(hsh = {}, &blk)
(blk || hsh)["bar"]
end

You could even see procs and hashes as being the same:

class Proc
def to_hash
Hash.new{|key| hsh[key] = call(key)}
end
end

class Hash
def to_proc
proc{|key| self[key]}
end
end

hsh = {:a => 'foo', :b => 'bar', :c => 'baz'}
[:a, :b, :c].collect(&hsh) #=> ["foo", "bar", "baz"]

Yes, it's not too hard to deal with at all... I'm not sure I understand
the original request.

Personally, I think if we're going to consider making changes in this
area, I would like to see the introduction of a Block class - A Block being a
pre-evaluated Proc (simply a Block of code between '{' and '}') that can
be passed around and Proc'ified in different contexts (not that that
necessarily addresses the concerns of the
OP, though).



Phil
 
M

Martin DeMello

Trans said:
How is that? You have access via the parameter. Morevoer yield can
still function, so I do see how that that is the case. Hmm... I'm
starting to think I've been misunderstood.

you also lose the ability to call a block via yield without reifying it into
a proc object (something which, unless i'm much mistaken, currently
makes yield lighter-weight than proc#call)

martin
 
T

Trans

Hi,

Daniel said:
So what you're basically suggesting is making procs 1st class objects?

foo({|bar| ... })

No, that's a different question. Albeit one I like, but I think it has
been decided too problematic?
or that blocks should just be appended to the argument list?

def foo(a, b, block); end
foo:)a, :b){ ... }

Yes, this is what I mean.
Personally, I like how it work now -- oftentimes you need a variable
number of arguments and a block,

That's a good point. Although I think as of Ruby 1.9 such construct
will be possible. In any case it still can be possible. Eg. a fixed
number of end parameters after a variable count.
and the &block syntax is very easy to
use. Using hashes and procs interchangeably isn't hard at the moment,
either:

def foo(hsh = {}, &blk)
(blk || hsh)["bar"]
end

Although thechinically you would require an error catcher too, to
pevent both the hash and the block from being given at the same time
(note your example should be hsh=nil). As easy ias it seems it is far
from elegant and could be easier.

Condier also the simplification of Ruby syntax since the & parameter
hwould not be needed.

T.
 
T

Trans

Trans said:
"Problem" is relative. I can write progams in ASSEMBLER too, but I
CHOOSE to use Ruby. Right? Here you actually demonstrate what I mean by
showing what one has to do to get around the "problem". This might not
seem an issue, but consider the use case where your end user IS a
developer and you're asking him to define some methods to inteface to
your library. It is not behoving to ease of use, elegance to have to
require this kind of interface and subcode on every such method.
MOREOVER your's using a COINCIDENTAL similarity between a Hash and Proc
of the method []. Though my particular case generally needs the
polymorphism between Hash and Proc, this is but a specific case. My
point is too the general.

Eeek! Sorry about so many typos. Need to go back and edit my posts
more.

T.
 
T

Trans

Here are some "silly" examples that hopefully will help clarify what I
mean:

def foo1( x )
x[ 1 ]
end

foo1 { |x| x + 1 } #=> 2
foo1( 1 => 2 ) #=> 2

def foo2( n, x=lambda{|x| x + 1} )
x.call( n )
end

class ToS
def initialize( i ) ; @i = i ; end
def call( n ) ; @i.to_s + n.to_s ; end
end

foo2( 2 ) #=> 3
foo2( 2 ) { |x| x * 2 } #=> 4
foo2( 2, ToS.new(3) ) #=> "32"

# Notice how the parameter _requires_ the block.
def foo3( b )
yield
end
foo3 { "yep" } #=> "yep"

# to make it optional
def foo4( b=nil )
yield if block_given?
end

# or
def foo4( b=nil )
yield if b
end

# or of course even
def foo4( b=nil )
b.call if b
end

Hope that's enough to clarify my intent.

T.
 
D

Daniel Schierbeck

Trans said:
def foo2( n, x=lambda{|x| x + 1} )
x.call( n )
end

I guess default values could be added to the current syntax, i.e.

def foo(&blk = proc{ ... }); end


Daniel
 
R

Robert Klemme

Trans said:
Here are some "silly" examples that hopefully will help clarify what I
mean:

def foo1( x )
x[ 1 ]
end

foo1 { |x| x + 1 } #=> 2
foo1( 1 => 2 ) #=> 2

I find it not too complicated to accomplish this with the current
capabilities.

def foo1(x = nil, &b)
(b||x)[ 1 ]
end
def foo2( n, x=lambda{|x| x + 1} )
x.call( n )
end

class ToS
def initialize( i ) ; @i = i ; end
def call( n ) ; @i.to_s + n.to_s ; end
end

foo2( 2 ) #=> 3
foo2( 2 ) { |x| x * 2 } #=> 4
foo2( 2, ToS.new(3) ) #=> "32"

I can see how providing a block like argument can be helpful, but I
don't see any practical example where I would need this functionality.
Do you have something specific in mind?
# Notice how the parameter _requires_ the block.
def foo3( b )
yield
end
foo3 { "yep" } #=> "yep"

So you mean that having b in the parameter list triggers an exception if
the block is missing?
# to make it optional
def foo4( b=nil )
yield if block_given?
end

# or
def foo4( b=nil )
yield if b
end

# or of course even
def foo4( b=nil )
b.call if b
end

Hope that's enough to clarify my intent.

I understand your intent - but I'm not convinced that it's an
improvement. The problem with all these language changes is IMHO that
they need to provide serious improvements to outweigh incompatibilities
and the need for code rewrite.

Removing &b and merging the block parameter with the other parameters
removes the distinction between these two fundamentally different types
of parameters. One consequence is that you cannot enforce a single
parameter method any more. Today def foo(x)...end defines a method that
is required to receive a single argument. You cannot do that any more
with your solution. Instead the method will happily accept an argument
or a block. Also, if neither are missing you will get a strange error,
because the correct message would be something like "parameter or block
missing". I consider that a step backwards. Today you get specific errors.

Another downside is the case with arbitrary length parameter lists.
With your change that code actually becomes more complex and less efficient:

def foo(*a)
a = a[0..-2] if block_given?
a.each ...
end

IMHO this situation is more common than your scenario but YMMD.

Regards

robert
 
T

Trans

Robert said:
Trans said:
Here are some "silly" examples that hopefully will help clarify what I
mean:

def foo1( x )
x[ 1 ]
end

foo1 { |x| x + 1 } #=> 2
foo1( 1 => 2 ) #=> 2

I find it not too complicated to accomplish this with the current
capabilities.

def foo1(x = nil, &b)
(b||x)[ 1 ]
end

I can see how providing a block like argument can be helpful, but I
don't see any practical example where I would need this functionality.
Do you have something specific in mind?

You say it is not _too_ complicated, but clearly it it requires
additional and specialized consideration. And if one were to do it
precicely it would have to be:

def foo1(x = nil, &b)
if x and b
raise ArgumentError, 'both an object and block have been given
for a single parameter"
end
(b||x)[ 1 ]
end

So while one can say it is not _too_ complicated (esspeically for an
accomplished coder such as yourself) it is nonetheless a complication.
In my particular case it is really unaccetable b/c I'm not the one
writing the code. It is an interface that I want others to use and
write there own code --hence complications of this sort get in the way.
Specifically I want end users to write there own task generators like
so:

def mytask( name, data )
desc "This is my task"
task name do
SomeTask.new( data )
end
end

This needs to work from either a script, like this:

mytask :foo do |t|
t.bar = 'and so on'
end

or evaluated based on an entry in a YAML file.

foo: !!mytask
bar: and so on

Hence in this case a hash would be passed to the method. Now, obviously
I can do all sorts of work arounds. But it was in working on this issue
that I realized that there exists this lack of polymorphism with regard
to the block paramater and that it really doesn't need to exist. Hence
I proposed this idea. And I suspect many other uses would arise if it
were available.
So you mean that having b in the parameter list triggers an exception if
the block is missing?

Correct. By having the argument the method requires a parameter. But
that parameter could be a block. If no paramter is given plainly there
would be an ArgumentError, namely

ArgumentError: wrong number of arguments (0 for 1)
I understand your intent - but I'm not convinced that it's an
improvement. The problem with all these language changes is IMHO that
they need to provide serious improvements to outweigh incompatibilities
and the need for code rewrite.

I agree. Personally I think it a pretty considerable improvement.
Polymorphism is a very useful feature as all OO programmers know.
Opening that possiblity up to blocks can only be a good thing in that
regard. Does it out way the downside? Well, I look at the other
advantages. For instance it actually simplifies the syntax of a method
interface. There's no need to explain the use of & --the block just
goes to the last parameter. There's no need for that special :yield:
token for RDoc --and there's no confusion if that token is forgotten.
There's nothing hidden about the method signiture, hence more self
documenting. And you can require the the block be given or provide a
default. Those are pretty good upsides.
Removing &b and merging the block parameter with the other parameters
removes the distinction between these two fundamentally different types
of parameters. One consequence is that you cannot enforce a single
parameter method any more. Today def foo(x)...end defines a method that
is required to receive a single argument. You cannot do that any more
with your solution. Instead the method will happily accept an argument
or a block.

I appears to me that in this respect your conception of the issue is
being constrained by previous expectation. The whole point is that such
a distinction is not fundamental but arbitrary and in being so goes
against the notions of polymorphism and duck-typing. Indeed the
expected behavor of def foo(x) is to take a single parameter --and a
BLOCK IS A PARAMETER. Conversely, as it stand today, all methods can
take at lease one parameter --a block can be passed to any method
regardless of whether it is useful or not.

def g ; 1 ; end
g { |x| (x * 32 + 12).flip "not a dang flipping thing" }
g #=> 1

Code obfusicators have at it! ;-)
Also, if neither are missing you will get a strange error,
because the correct message would be something like "parameter or block
missing". I consider that a step backwards. Today you get specific errors.

Oh no. It's a simple argument error. As I gave above.

ArgumentError: wrong number of arguments (0 for 1)

Not strange at all. Again, the block is a parameter. Just because it
gets special treatment doesn't make it otherwise.
Another downside is the case with arbitrary length parameter lists.
With your change that code actually becomes more complex and less efficient:

def foo(*a)
a = a[0..-2] if block_given?
a.each ...
end

Not so, in future version of Ruby it will be possible to do:

def foo(*a, b)
a.each ...
b ...
end

If I recall correctly, I beleive this behavior is already planned.

T.
 

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
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top