3

We create a method with splatted arguments and call Method#parameters on it:

def splatter(x, *y, z); end

params = method(:splatter).parameters
  # => [[:req, :x], [:rest, :y], [:req, :z]]

I'm looking for a function f that will map a list of arguments onto their corresponding variable names. The function should be flexible enough to work on any other method with arbitrarily placed splat arguments. For example:

args = [:one, :two, :three, :four]

f(params, args)
  # => [[:x, :one], [:y, :two], [:y, :three], [:z, :four]]

or something along those lines (flipped elements would be fine also). I feel there must be a flexible, elegant solution using inject or something, but I can't seem to come up with it.

rickyrickyrice
  • 577
  • 3
  • 14
  • 1
    Interesting question. For arbitrary methods, there might be something you can do with the information you can get from an instruction sequence object (`RubyVM::InstructionSequence.of(method(:splatter))`). If you call `to_a`, the 12th element contains indexes of the various argument configurations. Unfortunately, I'm too tired to think about this beyond that point, but hopefully that might be helpful. – Zach Kemp Oct 01 '13 at 05:40
  • Nice, I didn't know you could do that... I'll have to dig deeper into InstructionSequence to see what information I can get out of it. Thanks! – rickyrickyrice Oct 01 '13 at 05:47
  • 1
    If you are looking for test cases, you may find [this answer of mine](http://stackoverflow.com/a/17173107/2988) useful. – Jörg W Mittag Oct 01 '13 at 10:06

3 Answers3

3
def f(params,*args)
    # elements to be assigned to splat parameter
    splat = args.count - params.count + 1

    # will throw an error if splat < 0 as that means not enough inputs given        
    params.map{ |p|     

            [ p[1] , ( p.first == :rest ? args.shift(splat) : args.shift  ) ]

           }
end

Examples

def splatter(x,*y,z)
    # some code
end

f(method(:splatter).parameters, 1,2,3,4)
#=>[[:x, 1], [:y, [2, 3]], [:z, 4]]

def splatter(x,y,*z)
    # some code
end

f(method(:splatter).parameters, 1,2,3,4)
# => [[:x, 1], [:y, 2], [:z, [3, 4]]]

def splatter(x,*z)
    # some code
end

f(method(:splatter).parameters, 1)
# => [[:x, 1], [:z, []]]
tihom
  • 7,923
  • 1
  • 25
  • 29
  • I downvoted your question accidentally but can't remove my vote unless you edit your answer. Sorry for that! – Patrick Oscity Oct 01 '13 at 13:02
  • This is definitely a step in the right direction I think. It doesn't give quite the answer I want, but it wouldn't be hard to adjust it. I'm going to wait a little longer to see if someone can come up with anything else. Call it a hunch, but I really feel like there's a simpler solution. – rickyrickyrice Oct 01 '13 at 16:06
  • @rickyrickyrice it can be modified to your format. Was not sure what should the results look like if splat argument is `blank` or `[]`? – tihom Oct 01 '13 at 17:02
  • Very nice! Reads like a book. Splat, I love you. – Cary Swoveland Oct 06 '13 at 15:25
2

I think this is a good example where eval can be useful. The code below generates a lambda which takes the same arguments as specified and spits out the resolved list of arguments. The advantage of this approach is that Ruby's own algorithm for resolving splats is used.

def resolve(parameters,args)
  param_list = parameters.map do |type,name|
    splat = '*' if type == :rest
    "#{splat}#{name}"
  end.join(',')

  source = ""
  source << "->(#{param_list}) do\n"
  source << "  res = []\n"
  parameters.each do |type,name|
    if type == :rest
      source << "  res += #{name}.map {|v| [:'#{name}',v] }\n"
    else
      source << "  res << [:'#{name}',#{name}]\n"
    end
  end
  source << "end"

  eval(source).call(*args)
end

Example:

params = ->(x,*y,z){}.parameters
resolve(params,[:one, :two, :three, :four])
#=> [[:x, :one], [:y, :two], [:y, :three], [:z, :four]]

Behind the scenes, the following code was generated:

->(x,*y,z) do
  res = []
  res << [:'x',x]
  res += y.map {|v| [:'y',v] }
  res << [:'z',z]
end

Another example with two arguments, splat first:

params = ->(*x,y){}.parameters
resolve(params,[:one, :two, :three, :four])
#=> [[:x, :one], [:x, :two], [:x, :three], [:y, :four]]

With the generated code being

->(*x,y) do
  res = []
  res += x.map {|v| [:'x',v] }
  res << [:'y',y]
end
Patrick Oscity
  • 53,604
  • 17
  • 144
  • 168
  • Very interesting approach! Definitely giving me some insight into the problem domain. This will probably sound annoyingly hand-wavy, but I'm really looking for something elegant that involves functional operations like a fold, map, or zip of some sort. But thanks for the suggestion! – rickyrickyrice Oct 01 '13 at 16:16
  • good approach +1, has the potential of being very generic (including block inputs, options parameters etc), there is an old plugin along same line: [merb-action-args](https://github.com/merb/merb/tree/master/merb-action-args) – tihom Oct 01 '13 at 18:18
0

Edit: After my initial confusion:

def doit(params, args)
  rest_ndx = params.map(&:first).index(:rest)
  to_insert = [params[rest_ndx].last]*(args.size-params.size) if rest_ndx
  params = params.map(&:last)
  params.insert(rest_ndx,*to_insert) if rest_ndx
  params.zip(args)
end
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100