Project

General

Profile

Actions

Feature #4610

closed

Proc#curry behavior is inconsistent with lambdas containing default argument values

Added by jballanc (Joshua Ballanco) over 13 years ago. Updated over 12 years ago.

Status:
Rejected
Target version:
-
[ruby-core:35879]

Description

If I curry a lambda with 3 arguments, then I can call three times with one argument each time to get the desired results:

ruby-1.9.2-p180 :001 > l = ->(a, b, c) { puts "#{a}, #{b}, #{c}" }
#<Proc:0x00000100963650@(irb):1 (lambda)>
ruby-1.9.2-p180 :002 > c = l.curry
#<Proc:0x0000010095c9e0 (lambda)>
ruby-1.9.2-p180 :003 > c.('one').('two').('three')
one, two, three
nil

However, if the lambda has default values and I curry it, the entire lambda is evaluated after the first #call:

ruby-1.9.2-p180 :004 > l = ->(a = 'ichi', b = 'ni', c = 'san') { puts "#{a}, #{b}, #{c}" }
#<Proc:0x00000100877b88@(irb):4 (lambda)>
ruby-1.9.2-p180 :005 > c = l.curry
#<Proc:0x0000010086b338 (lambda)>
ruby-1.9.2-p180 :006 > c.('one').('two').('three')
one, ni, san
NoMethodError: undefined method `call' for nil:NilClass

This behavior seem very inconsistent. Ideally, if I wanted to use the default argument at a certain position in a currie proc, I would just #call with no arguments, like so:

ruby-1.9.2-p180 :007 > c.('one').().('three')
#=> Propose that this result in: "one, ni, three"

Updated by jballanc (Joshua Ballanco) over 13 years ago

=begin
Appologies for the formatting mistakes...

If I curry a lambda with 3 arguments, then I can call three times with one argument each time to get the desired results:

ruby-1.9.2-p180 :001 > l = ->(a, b, c) { puts "#{a}, #{b}, #{c}" }
ruby-1.9.2-p180 :002 > c = l.curry
ruby-1.9.2-p180 :003 > c.('one').('two').('three')
one, two, three nil

However, if the lambda has default values and I curry it, the entire lambda is evaluated after the first #call:

ruby-1.9.2-p180 :004 > l = ->(a = 'ichi', b = 'ni', c = 'san') { puts "#{a}, #{b}, #{c}" }
ruby-1.9.2-p180 :005 > c = l.curry
ruby-1.9.2-p180 :006 > c.('one').('two').('three')
one, ni, san
NoMethodError: undefined method `call' for nil:NilClass

This behavior seem very inconsistent. Ideally, if I wanted to use the default argument at a certain position in a currie proc, I would just #call with no arguments, like so:

ruby-1.9.2-p180 :007 > c.('one').().('three')
#=> Propose that this should result in: "one, ni, three"
=end

Updated by mame (Yusuke Endoh) over 13 years ago

  • Target version set to 3.0

=begin
Hello,

2011/4/25 Joshua Ballanco :

This behavior seem very inconsistent.

Indeed, it is arguable how Proc#curry should handle optional parameters,
but I don't think it is inconsistent. In either case, a curried Proc
fires its execution as soon as required arguments are given.

At least, this is intended, not a bug.
So I'm moving this ticket to 2.0 feature request tracker.

Ideally, if I wanted to use the default argument at a certain position in a currie proc, I would just #call with no arguments, like so:

ruby-1.9.2-p180 :007 > c.('one').().('three')
#=> Propose that this result in: "one, ni, three"

It is interesting, but incompatible with the current behavior.
And I can think that it will be even inconsistent:

c.('one', 'two').( 'three') #=> one, two, three
c.('one') .( 'two', 'three') #=> one, two, three
c.() .('one', 'two', 'three') #=> wrong number of arguments

--
Yusuke Endoh
=end

Actions #3

Updated by mame (Yusuke Endoh) over 13 years ago

  • Tracker changed from Bug to Feature

=begin

=end

Updated by jballanc (Joshua Ballanco) over 13 years ago

=begin
Ok, making this a feature request for 2.0 sounds like a good idea.

Regarding the consistency argument, as I understand Currying (or at least the way that it is implemented in most other languages), the result of a Proc#curry call should be a chain of Proc's with arity 1 that return Proc's with arity 1 until all arguments have been satisfied. It would be nice if Ruby behaved similarly.

For example, in OCaml (which auto-curries functions):

let foo a b c =

print_endline (String.concat ", " [a; b; c])
;;
val foo : string -> string -> string -> unit =

foo "first" ;;

  • : string -> string -> unit =

foo "first" "second" ;;

  • : string -> unit =

foo "first" "second" "third" ;;

first, second, third

  • : unit = ()

=end

Updated by mame (Yusuke Endoh) over 13 years ago

=begin
Hello,

2011/4/26 Joshua Ballanco :

Regarding the consistency argument, as I understand Currying (or at least the way that it is implemented in most other languages), the result of a Proc#curry call should be a chain of Proc's with arity 1 that return Proc's with arity 1 until all arguments have been satisfied. It would be nice if Ruby behaved similarly.

How should Ruby handle *rest parameter?

proc {|x, y, z, *rest| }.curry.(1).(2).(3).(4).(5)... ?

For example, in OCaml (which auto-curries functions):

If you quote OCaml, you should note that Ocaml also provides
optional arguments.
Actually, OCaml handles optional arguments as Ruby does.
IOW, OCaml function also fires as soon as all the required
arguments are given:

let foo ?(a

=end

Updated by jballanc (Joshua Ballanco) over 13 years ago

=begin
On Tue, Apr 26, 2011 at 7:57 AM, Yusuke ENDOH wrote:

Hello,

2011/4/26 Joshua Ballanco :

Regarding the consistency argument, as I understand Currying (or at least
the way that it is implemented in most other languages), the result of a
Proc#curry call should be a chain of Proc's with arity 1 that return Proc's
with arity 1 until all arguments have been satisfied. It would be nice if
Ruby behaved similarly.

How should Ruby handle *rest parameter?

proc {|x, y, z, *rest| }.curry.(1).(2).(3).(4).(5)... ?

I agree rest is a complication, but terminal rest seems to be ok. I would
argue that, in the case you provide, the final proc yielded could have arity
-1.

proc {|x, y, z, *rest| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4)

=> "1, 2, 3, 4"

proc {|x, y, z, *rest| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4, 5)

=> "1, 2, 3, 4, 5"

proc {|x, y, z, *rest| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4).(5)

=> "1, 2, 3, 4"

=> NoMethodError: undefined method `call' for nil:NilClass

The real problem is interstitial rest argument, but even here I would argue
that at the point where the rest argument is encountered, the proc should
have arity -1 (and keeping arity strictness between lambda and proc):

proc {|x, y, *rest, z| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4)

=> "1, 2, 4, 3"

lambda {|x, y, *rest, z| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4, 5)

=> ArgumentError: wrong number of arguments (2 for 1)

proc {|x, y, *rest, z| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4, 5)

=> "1, 2, 4, 3"

Essentially, if you keep to the notion of currying pulling argument lists
apart and creating new methods for each argument, I think this is still
doable in Ruby.

For example, in OCaml (which auto-curries functions):

If you quote OCaml, you should note that Ocaml also provides
optional arguments.
Actually, OCaml handles optional arguments as Ruby does.
IOW, OCaml function also fires as soon as all the required
arguments are given:

let foo ?(a="ichi") ?(b="ni") ?(c="san") () =

 print_endline (S(String.concat ", " [a; b; c]);;

val foo : ?a:string -> ?b:string -> ?c:string -> unit -> unit =

foo ();;

ichi, ni, san

  • : unit = ()

foo ~a:"first" ();;

first, ni, san

  • : unit = ()

foo ~a:"first" ~b:"second" ();;

first, second, san

  • : unit = ()

foo ~a:"first" ~b:"second" ~c:"third" ();;

first, second, third

  • : unit = ()

There are some differences between OCaml and Ruby:

  • OCaml function requires at least one mandatory argument.
    (In this case, () is the only mandatory argument.)

  • Optional arguments always requires labels (= keywords).

I believe your concern (and #4601) will be solved by keyword
arguments.

def foo(a:"ichi", b:"ni", c:"san")
puts "#{ a }, #{ b }, #{ c }"
end

foo(b:"second") #=> ichi, second, san

method(:foo).curry.
pass_option(a: "first").
pass_option(b: "second").
pass_option(c: "third").
call() #=> first, second, third

Unfortunately, a new method (Proc#pass_option) is needed
because Proc#call(key: val) passes a hash { key => val } as
a normal argument, unless we accept the incompatibility.

This is an interesting approach I hadn't considered. I agree that OCaml's
approach works because of the requirement of naming optional arguments (so,
for example, I can still pass just the second of three). This also makes me
wonder if keyword arguments and currying might not be more related than I
had thought.

Forgive me for speculating, but what if a curried proc could remember the
variable name for its argument? This could provide a mechanism for keyword
arguments. In other words, assuming you could do:

c = lambda {|first, second="bar"| puts "#{first}, #{second}" }.curry

=> #<CurriedProc(lambda)>

c.argument_key

=> "first"

c = c.('foo')
c.argument_key

=> "second"

c.default_value

=> "bar"

c.("baz")

=> "foo, baz"

Then you could conceptually implement keyword arguments like so:

class KeywordProc
def initialize(curried_proc)
@curried = curried_proc
end

def call(args)
c = @curried.dup
while c.kind_of? Proc do
arg = args[c.argument_key]
if arg.nil?
if c.default_value
c = c.()
else
raise ArgumentError
end
end
c = c.(arg)
end
end
end

l = lambda { |first, second="san", third="three"| puts "#{first}, #{second},
#{third}" }
k = KeywordProc.new(l.curry)
k.call(second: "dos", first: "bir")

=> "bir, dos, three"

The future of keyword arguments is promised by matz
[ruby-core:32131]:

Keyword arguments will be available on 2.0.

I look more and more forward to 2.0 every day, now!

Cheers,

Josh
=end

Actions #7

Updated by naruse (Yui NARUSE) about 13 years ago

  • Project changed from Ruby master to 14
  • Target version deleted (3.0)
Actions #8

Updated by naruse (Yui NARUSE) about 13 years ago

  • Project changed from 14 to Ruby master

Updated by mame (Yusuke Endoh) over 12 years ago

  • Description updated (diff)
  • Status changed from Open to Assigned
  • Assignee set to mame (Yusuke Endoh)

Updated by mrkn (Kenta Murata) over 12 years ago

  • Description updated (diff)

Updated by mame (Yusuke Endoh) over 12 years ago

  • Status changed from Assigned to Rejected

Hello,

I don't think it is a good idea to solve the original problem by using Proc#curry.
And I like the current behavior of Proc#curry. It is simple and straightforward.

jballanc (Joshua Ballanco) wrote:

proc {|x, y, z, *rest| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).(4)

=> "1, 2, 3, 4"

It will require an extra call for invoking the proc when we want to supply no argument to the rest:

proc {|x, y, z, *rest| puts "#{x}, #{y}, #{z}, #{rest.join(',')}"
}.curry.(1).(2).(3).()

=> "1, 2, 3, "

Anyway, keyword argument is implemented in trunk now. Please give it a try first. After that, if you still think you need anything, please reopen this ticket with clear statement about what you need.

Thanks,

--
Yusuke Endoh

Updated by jballanc (Joshua Ballanco) over 12 years ago

=begin
I have just tried with 2.0 and keyword args, and I think the situation is now more confusing. Now, if one wants to pass alternate values for optional arguments, the only way to do so it with the last call like so:

>> l = ->(a, b, foo: 'hello', bar: 'world') { puts a, b, "#{foo}, #{bar}" }
=> #<Proc:0x007ffce11d96e8@(irb):1 (lambda)>
>> l.(1,2)
1
2
hello, world
=> nil
>> c = l.curry
=> #<Proc:0x007ffce11e10c8 (lambda)>
>> c = c.(1)
=> #<Proc:0x007ffce11ea970 (lambda)>
>> c.(bar: 'moon')
ArgumentError: wrong number of arguments (1 for 2)
	from (irb):1:in `block in irb_binding'
	from (irb):5:in `call'
	from (irb):5
	from ./bin/irb:12:in `<main>'
>> c.(2)
1
2
hello, world
=> nil
>> c.(2, bar: 'moon', foo: 'goodnight')
1
2
goodnight, moon
=> nil

Unfortunately, there is no way to know when one is about to make the last call on a curried proc, since the arity is always reported as -1:

>> l.arity
=> 2
>> c = l.curry
=> #<Proc:0x007ffce12008d8 (lambda)>
>> c.arity
=> -1
>> c = c.(1)
=> #<Proc:0x007ffce1208a88 (lambda)>
>> c.arity
=> -1

I very much like the proposal for a Proc#pass_option method. Should this be reopened or shall I file a separate bug for the implementation of Proc#pass_option?
=end

Updated by mame (Yusuke Endoh) over 12 years ago

Hello,

jballanc (Joshua Ballanco) wrote:

I very much like the proposal for a Proc#pass_option method. Should this be reopened or shall I file a separate bug for the implementation of Proc#pass_option?

It would be good to open another feature ticket.

We should find the better name than pass_option and discuss
use case (or, the reason why you want to use Proc#curry with
keyword argument).

--
Yusuke Endoh

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0