更改 ruby 中块内的上下文/绑定

问题描述 投票:0回答:5

我在 Ruby 中有一个 DSL,其工作原理如下:

desc 'list all todos'
command :list do |c|
  c.desc 'show todos in long form'
  c.switch :l
  c.action do |global,option,args|
    # some code that's not relevant to this question
  end
end

desc 'make a new todo'
command :new do |c|
  # etc.
end

一位开发人员同事建议我增强 DSL,使其不需要将

c
传递到
command
块,因此不需要所有的
c.
里面的方法;据推测,他暗示我可以使以下代码同样工作:

desc 'list all todos'
command :list do
  desc 'show todos in long form'
  switch :l
  action do |global,option,args|
    # some code that's not relevant to this question
  end
end

desc 'make a new todo'
command :new do
  # etc.
end

command
的代码看起来像

def command(*names)
  command = make_command_object(..)
  yield command                                                                                                                      
end

我尝试了几件事,但无法让它发挥作用;我不知道如何将

command
块内代码的上下文/绑定更改为与默认值不同。

关于这是否可能以及我如何做到这一点有什么想法吗?

ruby metaprogramming block
5个回答
37
投票

粘贴此代码:

  def evaluate(&block)
    @self_before_instance_eval = eval "self", block.binding
    instance_eval &block
  end

  def method_missing(method, *args, &block)
    @self_before_instance_eval.send method, *args, &block
  end

欲了解更多信息,请参阅这篇非常好的文章这里


14
投票

也许

def command(*names, &blk)
  command = make_command_object(..)
  command.instance_eval(&blk)
end

可以在命令对象的上下文中评估块。


6
投票
class CommandDSL
  def self.call(&blk)
    # Create a new CommandDSL instance, and instance_eval the block to it
    instance = new
    instance.instance_eval(&blk)
    # Now return all of the set instance variables as a Hash
    instance.instance_variables.inject({}) { |result_hash, instance_variable|
      result_hash[instance_variable] = instance.instance_variable_get(instance_variable)
      result_hash # Gotta have the block return the result_hash
    }
  end

  def desc(str); @desc = str; end
  def switch(sym); @switch = sym; end
  def action(&blk); @action = blk; end
end

def command(name, &blk)
  values_set_within_dsl = CommandDSL.call(&blk)

  # INSERT CODE HERE
  p name
  p values_set_within_dsl 
end

command :list do
  desc 'show todos in long form'
  switch :l
  action do |global,option,args|
    # some code that's not relevant to this question
  end
end

将打印:

:list
{:@desc=>"show todos in long form", :@switch=>:l, :@action=>#<Proc:0x2392830@C:/Users/Ryguy/Desktop/tesdt.rb:38>}

2
投票

我编写了一个类来处理这个确切的问题,并处理 @instance_variable 访问、嵌套等问题。这是另一个问题的写法:

Ruby on Rails 中的块调用


0
投票

@Jatin Ganhotra 的答案似乎更准确,但需要根据问题进行调整并提供更多信息。

以下是经过调整的解决方案

module DslAble
  # It runs the `block` within this object context
  # @note if the object misses any method, redirects the method to the
  #   original evaluate caller.
  # Parameters are passed to the block
  def evaluate(*args, **kargs, &block)
    @self_before_evaluate = eval "self", block.binding
    instance_exec(*args, **kargs, &block).tap do
      @self_before_evaluate = nil
    end
  end

  # When it's the case, redirect to the original `evaluate` caller
  # @see https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
  def method_missing(method, *args, **kargs, &block)
    super unless @self_before_evaluate
    @self_before_evaluate.send(method, *args, **kargs, &block)
  end
end

有两项主要修正和一项改进:

  • (修正)一旦块被评估,实例变量应该被设置回
    nil
  • (修正)当我们不在
    method_missing
    调用的上下文中时,应以
    evaluate
    的默认行为为准(这就是我们调用
    super
    的地方)
  • (改进)
    instance_exec
    允许将参数传递给
    block
    。这样我们就可以使用块接收的参数来调用
    evaluate
    ,最终用户可能仍然想使用它们(并且为了与现有定义向后兼容)。

在问题的场景中,假设有一个类

Command
。您可以在其中包含此模块:

class Command
  include DslAble

  def self.make_object(*names); some_logic_here; end

  def desc(str);    @last_desc = str; end
  def switch(sym);  @switches << Switch.new(sym).desc(@last_desc) ; end
  def action(&blk); @switches.last.action(&blk); end
end

然后,顶层

command
方法将定义如下:

def command(*names, &block)
  command = Command.make_object(*names) # omitted in the question
  command.evaluate(command, &block)                                                                    
end
  • 观察您将
    command
    作为参数传递给块。 向后兼容与以前的定义,其中
    command
    明确地称为块的参数。

您为

command
创建的新块将隐式引用您的
Command
对象的方法,从而使以下内容按您的预期工作:

desc 'list all todos'
command :list do
  desc 'show todos in long form'
  switch :l
  action do |global,option,args|
    # some code that's not relevant to this question
  end
end

跨上下文冲突的方法

必须注意的是,当使用

missing_method
时,带有
evaluate
钩子的方法将引用回原始调用者。这意味着在
command
块中,您应该能够引用该块的原始上下文中可用的方法(即
argument
)。但是,如果该方法也存在于您的
Command
对象中,则会调用它:

desc 'list all todos'
command :list do
  desc 'the long form'
  switch: :l
  action { |*args| do_whatever }

  # Nested command definition
  desc "this desc does not define below's command, but override switch's one :/"
  command :to_csv { do_some_stuff }
end
  • 虽然第二个
    command
    将通过
    missing_method
    正确调用(从主上下文),但此方法将无法将最后一个
    desc
    描述链接到嵌套的
    command
    ,因为
    desc
    作为
    Command存在
    方法(并且
    command
    对象位于其块内
    self
    )。
  • 因此,在这种特定用法(嵌套)中,它不向后兼容,除非您像
    rpec
    那样捕获和解析上下文。

在此更改之前,上述情况不会发生。但我想这是使用嵌套 DSL 引用冲突方法的一个正常问题(在本例中是

desc
)。

解决方法

您可以使用

command
方法的命名参数来解决此问题:

class Command
  def my_desc(str = :unused)
    return @desc if str == :unused
    @desc = str
  end
end

def command(*names, desc: nil, &block)
  command = Command.make_object(*names)
  command.my_desc(desc) if desc
  command.evaluate(command, &block)                                                                    
end

desc 'list all todos'
command :list do
  desc 'the long form'
  switch: :l
  action { |*args| do_whatever }

  # Nested command definition
  cdesc = "this desc does not define below's command, but override switch's one :/"
  command :to_csv, desc: cdesc { do_some_stuff }
end

还有其他选择,但这里不是主题。

© www.soinside.com 2019 - 2024. All rights reserved.