[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Polymorphic block parameters -- an idea for Ruby 2.0

Trans

5/30/2006 3:00:00 PM

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.

11 Answers

Robert Klemme

5/30/2006 3:37:00 PM

0

Trans wrote:
> 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

Daniel Schierbeck

5/30/2006 4:09:00 PM

0

Trans wrote:
> ... 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

Trans

5/30/2006 4:36:00 PM

0


Robert Klemme wrote:
> 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? ;-)

> > foo { "Quack" }
> >
> > were ssentially the same as doing:
> >
> > foo( lambda { "Quack" } )
>
> 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.

ptkwt

5/30/2006 5:09:00 PM

0

In article <447c6e2c$0$103$edfadb0f@dread16.news.tele.dk>,
Daniel Schierbeck <daniel.schierbeck@gmail.com> wrote:
>Trans wrote:
> > ... long post ...
>
>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

Martin DeMello

5/30/2006 5:09:00 PM

0

Trans <transfire@gmail.com> wrote:
>
> 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

Trans

5/30/2006 5:09:00 PM

0

Hi,

Daniel Schierbeck wrote:
> 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.

Trans

5/30/2006 5:13:00 PM

0


Trans wrote:
> "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.

Trans

5/30/2006 6:01:00 PM

0

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.

Daniel Schierbeck

5/30/2006 6:57:00 PM

0

Trans wrote:
> 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

Robert Klemme

5/30/2006 8:21:00 PM

0

Trans wrote:
> 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