Polymorphic block parameters -- an idea for Ruby 2.0

Discussion in 'Ruby' started by Trans, May 30, 2006.

  1. Trans

    Trans Guest

    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.
     
    Trans, May 30, 2006
    #1
    1. Advertising

  2. 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
     
    Robert Klemme, May 30, 2006
    #2
    1. Advertising

  3. 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
     
    Daniel Schierbeck, May 30, 2006
    #3
  4. Trans

    Trans Guest

    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.
     
    Trans, May 30, 2006
    #4
  5. Trans

    Phil Tomson Guest

    In article <447c6e2c$0$103$>,
    Daniel Schierbeck <> 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
     
    Phil Tomson, May 30, 2006
    #5
  6. Trans <> 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
     
    Martin DeMello, May 30, 2006
    #6
  7. Trans

    Trans Guest

    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, May 30, 2006
    #7
  8. Trans

    Trans Guest

    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, May 30, 2006
    #8
  9. Trans

    Trans Guest

    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.
     
    Trans, May 30, 2006
    #9
  10. 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
     
    Daniel Schierbeck, May 30, 2006
    #10
  11. 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
     
    Robert Klemme, May 30, 2006
    #11
  12. Trans

    Trans Guest

    Robert Klemme wrote:
    > 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
    >
    > 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.

    > > # 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?


    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)

    > > # 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.


    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.
     
    Trans, May 31, 2006
    #12
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. John Crowley
    Replies:
    2
    Views:
    491
    =?Utf-8?B?Sm9obiBDcm93bGV5?=
    Feb 6, 2004
  2. morrell
    Replies:
    1
    Views:
    980
    roy axenov
    Oct 10, 2006
  3. Replies:
    10
    Views:
    1,258
    Big K
    Feb 2, 2005
  4. Dr Mephesto

    App idea, Any idea on implementation?

    Dr Mephesto, Feb 4, 2008, in forum: Python
    Replies:
    3
    Views:
    733
    Dennis Lee Bieber
    Feb 5, 2008
  5. Replies:
    0
    Views:
    648
Loading...

Share This Page