Pregunta

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.

¿Fue útil?

Solución

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, []]]

Otros consejos

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

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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top