質問

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.

役に立ちましたか?

解決

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

他のヒント

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
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top