Это был очень интересный вопрос, поэтому, прежде всего, спасибо за это.
Учитывая, что Binding
является объектом, как и любой другой, мы можем построить класс для обеспечения функциональности этого типа и использовать привязку из исходного метода для делегирования всех аргументов следующему методу, например, так:
class ArgsBuilder
attr_reader
def initialize(b)
@standard_args, @kwargs = [],{}
@binding = b
build!
end
def delegate(m)
@binding.receiver.send(m,*@standard_args,**@kwargs,&@block)
end
private
def build!
set_block(&@binding.eval('Proc.new')) if @binding.eval('block_given?')
@binding.eval('method(__method__)')
.parameters.each do |type,name|
next if type == :block
val = @binding.local_variable_get(name)
if type =~ /key/
@kwargs.merge!(type == :keyrest ? val : {name => val})
else
type == :rest ? @standard_args.concat(val) : @standard_args << val
end
end
end
def set_block(&block)
@block = block
end
end
Использование:
class B
def some_method(a,b,c, *d, e:, f:, g: nil, **h)
ArgsBuilder.new(binding).delegate(:some_other_method)
end
def some_other_method(a,b,c, *d, e:, f:, g: , **h)
yield __method__ if block_given?
"I received: #{[a,b,c,d,e,f,g,h].inspect}"
end
end
B.new.some_method(1,2,3,4,5,6, e: 7, f: 8, p: 9, n: 10) do |m|
puts "called from #{m}"
end
# called from some_other_method
#=> "I received: [1, 2, 3, [4, 5, 6], 7, 8, nil, {:p=>9, :n=>10}]"
# a b c ----d---- e f g -------h-------
Кажется достаточно простым, чтобы быть помощником и обрабатывать именованные и анонимные блоки, проходящие через методы.
TL; DR
Это, очевидно, требует сопоставления или, по крайней мере, приемлемых подписей в делегированном методе, аналогично тому, как работает super
.Однако мы могли бы сделать это довольно далеко и создать классы для типов аргументов [:req,:opt,:rest,:keyreq,:key,:keyrest,:block]
, поместить их в коллекцию и затем опросить делегируемый метод, чтобы определить правильные аргументы для прохождения;однако я не уверен, что этот тип примера хорошо вписался бы в SO сообщение.
Дополнительное примечание: поскольку в initialize
вызывается build!
, привязка локальной переменной является статической в момент создания класса ArgsBuilder
.например,
def foo(n)
builder = ArgsBuilder.new(binding)
n = 17
builder.delegate(:bar)
end
def bar(n)
n
end
foo(42)
#=> 42
Однако
def foo(n)
n = 17
ArgsBuilder.new(binding).delegate(:bar)
end
foo(42)
#=> 17
Это не означает, что изменяемые объекты не могут быть изменены
def foo(n)
builder = ArgsBuilder.new(binding)
n.upcase!
builder.delegate(:bar)
end
foo('a')
#=> "A"
Очевидно, вы можете изменить это, просто переместив вызовдо build!