Project

General

Profile

Actions

Feature #4601

closed

Re-ordering method parameters.

Added by ngollan (Nicos Gollan) almost 13 years ago. Updated about 12 years ago.

Status:
Closed
Target version:
-
[ruby-core:35861]

Description

=begin
In my attempt to make an old gem (merb-action-args) work with MRI 1.9.2, Rubinius, and other implementations that depend on Method#parameters to retrieve a description of parameters, I hit a roadblock (the core reason is that it is not possible to retrieve parameter defaults). To quote the gem's README:

== Original Problem: Out of order defaults

class Foo < Merb::Controller
def bar(one, two = "dos", three = "tres")
"#{one} #{two} #{three}"
end
end

The interesting thing here is that hitting "/foo/bar?one=uno&three=three" will call foo("uno", "dos", "three"). In other words, the defaults can be in any order, and merb-action-args will figure out where to fill in the holes.

== Parameter Omission

This reordering feature means we need some way to omit parameters when calling a method, thus maintaining defaults. Originally, that was done by direct access to the AST, which is not really an option anymore with 1.9 and other VMs like Rubinius. One possible, but ugly syntax for straight omission would be:

class Pet
def quack(a, b='quack', c='woof')
"#{a} #{b} #{c}"
end
end

Pet.new.quack('meow', , 'whinny') #=> "meow quack whinny"

Now, IMO, this looks quite ugly, and would be hard to make work with the ubiquitous *args pattern.

== Currying Extension

Another way to enable that behaviour would be parameter reordering, i.e., some way to map a Proc with n parameters to a method with >=n parameters. For the problem I am facing, that would mean I could curry the called method in a way so that all missing parameters are shifted to the end, and calling the resulting method with an appropriate list would preserve defaults. For example:

Assume the parameters passed to the quack action of the Pet controller are values for a and c. Then define a curried method:

quack_proxy =
pet_instance.method(:quack).curry(
2, # number of parameters in the resulting method
[0, 2, 1] # parameter order of the called method
)

or alternatively use parameter names, omit defaults, ...:

quack_proxy =
pet_instance.method(:quack).curry(2, [:a, :c])

The resulting Proc quack_proxy would have the signature:

quack_proxy(a, c='woof', b='quack')

quack_proxy.call(*['this', 'works'])

=> "this quack works"

== Proposed Currying Syntax/Semantics

(Proc) (({Method#curry(num_args, order = nil)}))

Returns a proxy with num_args arguments to the receiver.

When order is nil, the parameters of the receiver are mapped to the first num_args parameters of the proxy in order.

When order is an Array of integers or symbols with order.size >= num_args, it describes the order in which the parameters of the proxy are mapped to the receiver. Required parameters not named or indexed are mapped in order of their appearance.

In cases where not all required parameters of the receiver are covered, an ArgumentError is raised.

Examples:

def foo(req_a, req_b, opt_a=1, obt_b=2, opt_c=3); end
m = Kernel.method(:foo)

m.curry(2)

maps to foo(req_a, req_b) with all optional parameters at defaults

m.curry(3, [0, 1, :opt_b])

maps to foo(req_a, req_b, opt_b=2) with optionals a and c at defaults

m.curry(5, [0, 1, :opt_b])

maps to foo(req_a, req_b, opt_b=2, opt_a=1, opt_c=3)

m.curry(1, [:req_b])

raises ArgumentError (not all required parameters covered)

ret = m.curry(2, [:req_b])['hello']

maps to foo(req_a, 'hello') with all optionals at defaults

NOTE: this results in an unary method.

In the last example, the call to #curry creates a 2-ary Proc, which maps the first parameter to the req_b parameter of #foo, implicitly maps the second parameter to req_a, and hides all optional parameters. Then, by using #[] like Proc#[], the first parameter to the proxy is assigned. Since the second parameter is required, this returns an unary Proc.
=end

Updated by jballanc (Joshua Ballanco) almost 13 years ago

=begin
You can already do something similar with Method#to_proc and Proc#curry:

class Foo
def bar(a, b, c)
puts "a is #{a}"
puts "b is #{b}"
puts "c is #{c}"
end
end

f = Foo.new
m = f.method(:bar)
p = m.to_proc
c = p.curry
c.('first').(nil).('second')

=> Prints:

a is first

b is

c is second

Actually, in trying to replicate exactly what you wanted to do with Method#to_proc and Proc#curry, I noticed a bug in Proc#curry. If the proc contains default arguments, it will evaluate completely after the first #call, even when curried. I've filed a different issue to ask about that behavior (#4610).
=end

Updated by mame (Yusuke Endoh) almost 13 years ago

=begin
Hello,

I understand your problem, and I guess the current Ruby have no good way to solve it.
But I don't like your approach. It looks like an abuse of Proc#curry ;-)
What you guys want is partial application, not currying, I think.

As an alternative, a keyword argument may solve your problem.

--
Yusuke Endoh
=end

Updated by ngollan (Nicos Gollan) almost 13 years ago

=begin
Yusuke Endoh wrote:

But I don't like your approach. It looks like an abuse of Proc#curry ;-)

It was just meant as an idea. You might call it positional currying, partial application, or whatever ;-)

Joshua Ballanco wrote:

You can already do something similar with Method#to_proc and Proc#curry:

Your example shows the problem quite nicely, and that's what I am doing at the moment. It does not provide the functionality though.

I've filed a different issue to ask about that behavior (#4610).

That looks like it'd be hard to use with splatting, but I guess for my requirements it would work something like:

def foo(a, b = "bar", c="baz"); "#{a} #{b} #{c}"; end
l = ->(m, a, b, c) {puts m.call(a, b, c)}
c = l.curry
c.(method(:foo)).('dog').().('cat')

and deal with the parameter-less calls through a mixture of the incoming parameters hash and Method#parameters.

When calling it "partial application", I think something like

keep_me = method(:foo).apply(:a => 'dog', :c => 'cat')
#=> unary proc/lambda/method

...

5 minutes later

keep_me.call('bird')
#=> "dog bird cat"

would still be useful in some places where you don't want complete evaluation, since you'd otherwise have to manually keep track of missing parameters until you can fill in the gaps.
=end

Updated by mame (Yusuke Endoh) about 12 years ago

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

Updated by mame (Yusuke Endoh) about 12 years ago

  • Status changed from Assigned to Closed

Hello,

More or less, this problem is solved by keyword arguments, I guess.
So I close this ticket.

--
Yusuke Endoh

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0