我可以在它自己的上下文中创建一个 proc 吗?

Can I create a proc in the context of itself?

既然proc是一个对象,我可以在它自己的实例范围内创建一个proc吗?

例如:

prc = Proc.new do
  foo
end

def prc.foo
  123
end

prc.call
# NameError: undefined local variable or method `foo' for main:Object

通过更改 self 或明确 receiver 引用过程。

必须动态评估该接收器,例如以下应该有效:

other_prc = prc.clone

def other_prc.foo
  456
end

other_prc.call
#=> 456  <- not 123

这意味着我不能通过以下方式“硬编码”它:

prc = Proc.new do
  prc.foo
end

换句话说:有没有办法从 proc 中引用 procs 实例?


另一个没有 foo 的例子:(为 # ??? 放什么)

prc = Proc.new do
  # ???
end

prc == prc.call #=> true

other_prc = prc.clone
other_prc == other_prc.call #=> true

prc代替# ???只能满足prc == prc.call,不能满足other_prc == other_prc.call。 (因为 other_prc.call 仍然会 return prc

评论后编辑的第二次尝试

# This solution has a limit you have to return the `Proc` itself
with_proc = proc do |aproc, others|
  aproc.instance_variable_set(:@a, aproc.instance_variable_get(:@a) || 0)
  aproc.instance_variable_set(:@a, aproc.instance_variable_get(:@a) + 1)
  p self: aproc, arg: others, '@a': aproc.instance_variable_get(:@a)
  aproc
end

prc = with_proc.(with_proc, :foo)
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:foo, :@a=>1}

puts "prc: #{prc}"
puts "prc.equal?(with_proc): #{prc.equal?(with_proc)}"
# => prc: #<Proc:0x000055864be1a740@pro_self.rb:1>
# => prc.equal?(with_proc): true

prc.call(prc, :bar)
puts "prc @a: #{prc.instance_variable_get(:@a)}"
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:bar, :@a=>2}
# => prc @a: 2

other_prc = prc.call(prc.clone, :baz)
puts "other_prc: #{other_prc}"
# => {:self=>#<Proc:0x000055864be1a0b0@pro_self.rb:1>, :arg=>:baz, :@a=>3}
# => other_prc: #<Proc:0x000055864be1a0b0@pro_self.rb:1>


other_prc.call(other_prc, :qux)
#=> {:self=>#<Proc:0x000055864be1a0b0@pro_self.rb:1>, :arg=>:qux, :@a=>4}

prc.call(prc, :quux)
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:quux, :@a=>3}

使用此解决方案,您可以return任何需要的东西

prc = proc do |ref_to_self, others|
  self_reference = ref_to_self.instance_variable_get(:@ident)
  self_reference.instance_variable_set(:@a, self_reference.instance_variable_get(:@a) || 0)
  self_reference.instance_variable_set(:@a, self_reference.instance_variable_get(:@a) + 1)
  p ({self: self_reference.instance_variable_get(:@ident),
    arg: others,
    '@a': self_reference.instance_variable_get(:@a)})
end
prc.instance_variable_set(:@ident, prc)
prc.call(prc, :foo)

puts "prc: #{prc}"

prc.call(prc, :bar)
puts "prc @a: #{prc.instance_variable_get(:@a)}"

other_prc = prc.clone
other_prc.instance_variable_set(:@ident, other_prc)
other_prc.call(other_prc, :baz)
puts "other_prc: #{other_prc}"

other_prc.call(other_prc, :qux)

prc.call(prc, :quux)
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:foo, :@a=>1}
# prc: #<Proc:0x00005559f1f6d808@pro_self.rb:71>
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:bar, :@a=>2}
# prc @a: 2
# {:self=>#<Proc:0x00005559f1f6d1f0@pro_self.rb:71>, :arg=>:baz, :@a=>3}
# other_prc: #<Proc:0x00005559f1f6d1f0@pro_self.rb:71>
# {:self=>#<Proc:0x00005559f1f6d1f0@pro_self.rb:71>, :arg=>:qux, :@a=>4}
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:quux, :@a=>3}

第一次尝试

评论后编辑。我知道没有直接的方法来引用您传递给 new 的块内的 Proc 对象 我尝试使用 tap 来更接近您的代码。 我希望这可以帮助

def proc_reference_to_self(a_proc)
  first = Proc.new do
    puts "Hello"

  end.tap(&a_proc)
end

second_prc = Proc.new do |first|
  p first
  first.call
  puts "second_prc"
  p second_prc
end

# This execute second_prc as a block
proc_reference_to_self(second_prc)

# first and second are different objects but you can still reference first
# inside second_proc

# <Proc:0x000055603a8c72e8@ruby_array_of_paths.rb:75>
# Hello
# second_prc
# <Proc:0x000055603a8c7338@ruby_array_of_paths.rb:81>

利用闭包的外部范围

如果我正确理解你的问题,利用闭包的外部范围可能会达到你想要的效果。不可否认,这是一个非常人为的示例,它在数组中注册嵌套的 Proc 对象。在调用第一个 Proc 之前不会创建第二个 Proc,但它们都保留了与外部作用域的绑定。

@procs = []
@foo   = 1

@procs << proc do
  # Don't keep re-registering the nested Proc on
  # subsequent invocations.
  @procs << proc { @foo + 1 } unless @procs.count == 2
  @foo
end

@procs.map &:call
#=> [1, 2]

@foo = 3
@procs.map &:call
#=> [3, 4]

好的,现在我想我明白你的意思了。正如我在评论中提到的,它可以通过嵌套闭包来完成。因为 Procs/lambdas 是匿名的,闭包嵌套为 lambda 提供了一种接收对其自身的动态引用的方法,从而允许它在 self.[=16= 的上下文中 instance_eval 代码]

wrapped_dispatch = ->(f) { f[f] }

proc_wrapped = lambda do |myself|
  lambda do |n|
    myself.instance_eval do
      # in context of self
      bar(n)
    end
  end
end

def proc_wrapped.bar(n)
  p "bar #{n}"
end

wrapped_dispatch[proc_wrapped].call(123)
# => "bar 123"

# can also save it "unwrapped"
prc = wrapped_dispatch[proc_wrapped]

prc.call(123)
# => "bar 123"

# Very late binding to dynamic receiver
def proc_wrapped.bar(n)
  p "BAR #{n}"
end
prc.call(123)
# => "BAR 123"

# and if the "wrapped-ness" bothers you, link them together and delegate
proc_wrapped.define_singleton_method(:call) do |n|
  prc.call(n)
end

def proc_wrapped.bar(n)
  p "BBBBAAAARRRRR"
end
proc_wrapped.call(123)
# => "BBBBAAAARRRRR"

other_proc_wrapped = proc_wrapped.clone
wrapped_dispatch[other_proc_wrapped].call(123)
# => "BBBBAAAARRRRR"

def other_proc_wrapped.bar(n)
  p "foo #{n}"
end

wrapped_dispatch[other_proc_wrapped].call(123)
# => "foo 123"
proc_wrapped.call(123)
# => "BBBBAAAARRRRR"

我注意到这种行为非常类似于 class 的实例 (Foo.new) 与 class 的单例 class (Foo.singleton_class),这是有道理的,因为 closures and objects are equivalent。这意味着如果你真的想要这样的行为,你应该只使用 class、它的单例 class 和它的实例,就像 Ruby.

中惯用的那样

免责声明:我正在回答我自己的问题


解决方案非常简单。只需覆盖 call 即可通过 instance_exec:

调用过程

Executes the given block within the context of the receiver (obj). In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj's instance variables. Arguments are passed as block parameters.

prc = proc { |arg|
  @a ||= 0
  @a += 1
  p self: self, arg: arg, '@a': @a
}

def prc.call(*args)
  instance_exec(*args, &self)
end

这里,接收者是proc本身,“给定块”也是proc本身。 instance_exec 因此将在它自己的实例的上下文中调用 proc。它甚至会传递任何额外的参数!

使用上面的:

prc
#=> #<Proc:0x00007f84d29dcbb0>

prc.call(:foo)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:foo, :@a=>1}
#           ^^^^^^^^^^^^^^^^^^^^^^^^^^        ^^^^
#                  correct object          passes args

prc.call(:bar)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:bar, :@a=>2}
#                                                   ^^^^^^
#                                               preserves ivars

prc.instance_variable_get(:@a)
#=> 2 <- actually stores ivars in the proc instance

other_prc = prc.clone
#=> #<Proc:0x00007f84d29dc598>
#          ^^^^^^^^^^^^^^^^^^
#           different object

other_prc.call(:baz)
#=> {:self=>#<Proc:0x00007f84d29dc598>, :arg=>:baz, :@a=>3}
#                                                   ^^^^^^
#                                               ivars are cloned

other_prc.call(:qux)
#=> {:self=>#<Proc:0x00007f84d29dc598>, :arg=>:qux, :@a=>4}

prc.call(:quux)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:quux, :@a=>3}
#                                                    ^^^^^^
#                              both instances have separate ivars

通常在 DSL 中使用的一种通用方法称为 Clean Room 模式 - 为评估 DSL 代码块而构建的对象。它用于限制 DSL 访问不需要的方法,以及定义 DSL 工作的基础数据。

方法看起来像这样:

# Using struct for simplicity.
# The clean room can be a full-blown class. 
first_clean_room = Struct.new(:foo).new(123)
second_clean_room = Struct.new(:foo).new(321)

prc = Proc.new do
  foo
end

first_clean_room.instance_exec(&prc)
# => 123

second_clean_room.instance_exec(&prc)
# => 321

看来您正在寻找的是让 Proc 对象本身既充当块又充当洁净室。这有点不寻常,因为代码块通常是您希望在不同的基础数据上重用的内容。我建议您首先考虑原始模式是否更适合您的应用程序。

不过,将Proc对象作为clean room确实是可以做到的,而且代码看起来和上面的模式非常相似(代码看起来也和你在答案中贴出的方法相似):

prc = Proc.new do 
  foo
end

other = prc.clone

# Define the attributes in each clean room

def prc.foo
  123
end

def other.foo
  321
end

prc.instance_exec(&prc)
# => 123

other.instance_exec(&other)
# => 321

您还可以考虑通过创建一个继承自 Proc 的新 class 而不是覆盖本机 call 方法来使该方法更方便 运行。覆盖它本身并没有错,但是您可能需要灵活地将它附加到不同的接收器,因此这种方法可以让您同时拥有:

class CleanRoomProc < Proc
  def run(*args)
    instance_exec(*args, &self)
  end
end

code = CleanRoomProc.new do 
  foo
end

prc = code.clone
other = code.clone

def prc.foo
  123
end

def other.foo
  321
end

prc.run
# => 123

other.run
# => 321

如果您由于某种原因无法使用新的 class,例如您正在从 gem 获取 Proc 对象,您可以考虑使用模块扩展 Proc 对象:

module SelfCleanRoom
  def run(*args)
    instance_exec(*args, &self)
  end
end

code = Proc.new do 
  foo
end

code.extend(SelfCleanRoom)

prc = code.clone
other = code.clone

# ...