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

发布于 2024-11-04 09:34:57 字数 1058 浏览 8 评论 0原文

我在 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 块内代码的上下文/绑定更改为与默认值不同。

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

I have a DSL in Ruby that works like so:

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

A fellow developer suggested I enhance my DSL to not require passing c to the command block, and thus not require the c. for all
the methods inside; presumably, he implied I could make the following code work the same:

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

The code for command looks something like

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

I tried several things and was unable to get it to work; I couldn't figure out how to change the context/binding of the code inside the command block to be different than the default.

Any ideas on if this is possible and how I might do it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

看春风乍起 2024-11-11 09:34:57

粘贴此代码:

  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

有关更多信息,请参阅这篇非常好的文章 此处

Paste this code:

  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

For more information, refer to this really good article here

じ违心 2024-11-11 09:34:57

也许

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

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

Maybe

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

can evaluate the block in the context of command object.

柠栀 2024-11-11 09:34:57
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>}
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

Will print:

:list
{:@desc=>"show todos in long form", :@switch=>:l, :@action=>#<Proc:0x2392830@C:/Users/Ryguy/Desktop/tesdt.rb:38>}
一人独醉 2024-11-11 09:34:57

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

Block call in Ruby on Rails< /a>

I wrote a class that handles this exact issue, and deals with things like @instance_variable access, nesting, and so forth. Here's the write-up from another question:

Block call in Ruby on Rails

剑心龙吟 2024-11-11 09:34:57

@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)
    return unless block_given?
    @self_before_evaluate = eval "self", block.binding, __FILE__, __LINE__
    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 允许传递参数到。这样我们就可以使用块接收的参数调用评估,最终用户可能仍然想使用它们(并且为了与现有定义向后兼容)。

在问题的场景中,我们假设有一个类Command。您可以在其中包含此模块:

class Command
  include DslAble # << here

  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)
  command.evaluate(command, &block)                                                                    
end
  • 观察您将 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

: 。

必须注意的是,当使用 evaluate 时,带有 missing_method 钩子的方法将引用回原始调用者 这意味着在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

还有其他替代方法,但这里不是主题。

@Jatin Ganhotra answer seems the more accurate one, yet needs to be adapted to the question and provide some more info.

The below is an adapted solution

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)
    return unless block_given?
    @self_before_evaluate = eval "self", block.binding, __FILE__, __LINE__
    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

With two main amendments and one improvement:

  • (amendment) the instance variable should be set back to nil once the block has been evaluated
  • (amendment) the default behaviour of method_missing should prevail when we are not in the context of an evaluate call (that's where we call super)
  • (improvement) instance_exec allows to pass parameters to the block. This way we can call evaluate with params that would be received by the block, may the end user would still want to use them (and for backwards compatibility with existing definitions).

In the question's scenario, let's suppose there is a class Command. You would include this module in it:

class Command
  include DslAble # << here

  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

Then, the top level command method would be defined like this:

def command(*names, &block)
  command = Command.make_object(*names)
  command.evaluate(command, &block)                                                                    
end
  • Observe that you pass command as a parameter to the block. Backwards compatible with previous definitions where command is explicitly referred to as as an argument of the block.

New blocks you create for command will implicitly refer to methods of your Command object, making the below to work as you expected:

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

Cross-context clashed methods

It must be noted, that the approach with missing_method hook will refer back to the original caller, when evaluate has been used. This means that within the command block you are supposed to be able to refer to methods that were available in the original context of the block (i.e. argument). However, if that method also exists in your Command object, it will be called instead:

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
  • Although the 2nd command will be correctly called (from the main context) via missing_method, this approach will fail to link the last desc description to the nested command, because desc exists as a Command method (and the command object is self within its block).
  • So in this particular usage (nesting), it is NOT backwards compatible unless you capture and resolve contexts like rpec does.

Before this change, the above would not happen. But I guess that's a normal problem of using nested DSLs that refer to methods that clash (desc in this case).

Work Around

You could though work this around with named parameters to the command method:

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

There are other alternatives, but it is off topic here.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文