【问题标题】:Can I create a proc in the context of itself?我可以在自身的上下文中创建一个过程吗?
【发布时间】:2020-10-25 13:16:38
【问题描述】:

既然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 引用proc。

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

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 仍然会返回prc

【问题讨论】:

  • 虽然不是答案,但请注意:prc = Proc.new do; def self.foo; 123; end; foo; end; prc.call #=&gt; 123.
  • 另外,def foo; 123; end; prc = method(:foo).to_proc; prc.call =&gt; 123
  • @CarySwoveland 但那些foo 不是prc 的方法,是吗?
  • 嗯。第一名:prc.methods.include?(:foo) #=&gt; false。第二名:prc.method(:foo).owner #=&gt; Object.
  • 另一个想法:prc1 = proc { |l| @l = l; def self.foo; @l.call; end; foo }; prc1.call -&gt;{ 456 } 也有效,如果您试图保留延迟执行语义,这可能会有所帮助,但我不确定为什么这种间接级别是有益的。我知道您正在尝试动态定义您的 Proc 在执行时如何获取其值,但是当将文字作为参数注入或在绑定中引用数据对象时,似乎需要很长的路要走。

标签: ruby closures proc


【解决方案1】:

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


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

在接收者(obj)的上下文中执行给定的块。为了设置上下文,在代码执行时将变量self 设置为obj,从而使代码能够访问obj 的实例变量。参数作为块参数传递。

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

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

这里,接收者是过程本身,“给定块”也是过程本身。因此,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

【讨论】:

  • 感谢分享。我不得不承认我没有抓住重点。在这种Proc 中,self 会永远丢失,这取决于这段代码的目的是什么,您可能会找到一个不同的解决方案,让self 动态指向上下文。
  • @GiuseppeSchembri self 永远怀念”是什么意思
  • 抱歉,我的意思是在您用来创建Proc 的块内,this article 可能只是我们如何错过self 的一个例子。
  • @GiuseppeSchembri 当然,self 只能有一个。如果您还需要外部 self,您可以将其分配给一个变量并从 proc 中引用该变量,例如context = self ; prc = proc { p self: self, context: context }
  • 您可以通过调用 binding.eval('self') 从 proc 中获取原始(外部)self。原因是更改self 不会影响binding。因此,您始终可以使用binding 作为外部上下文的网关。
【解决方案2】:

通常在 DSL 中使用的通用方法称为洁净室模式 - 您构建的对象是为了评估 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 对象用作洁净室,并且代码看起来与上面的模式非常相似(代码看起来也与您在答案中发布的方法相似):

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 的新类而不是覆盖本机 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

如果你因为某种原因不能使用新的类,例如您从 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

# ...

【讨论】:

  • 实际上,我正在尝试找到解决another question 的方法。一个可以在实例变量中保持自己状态的过程看起来很有希望,但我找不到实现它的方法(因此这个问题)。最后,我solved it 使用了一个单独的对象来保存状态,这与您的“洁净室”方法非常相似。
【解决方案3】:

利用闭包的外部作用域

如果我正确理解您的问题,利用闭包的外部范围可能会满足您的需求。诚然,这是一个非常人为的示例,它将您的嵌套 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 &lt;&lt; proc { @foo },后跟@procs &lt;&lt; proc { @foo + 1 }。我正在尝试做的是从 proc 的块中动态访问 proc 自己的实例。
  • @Stefan 您可能能够在 ObjectSpace 中找到它,或访问当前绑定,但 #self 不返回 Proc(只是接收器,例如 @987654325 @) 直到 Proc#new 返回。有了对象引用,您可以 #send 或使用 Binding#eval 并将 Proc 作为接收器,但我仍然不清楚这会给您带来什么,因为命名空间中的任何闭包都只包含对变量的引用绑定,防止您为绑定中的同一变量分配不同的值。
【解决方案4】:

评论后编辑的第二次尝试

# 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}

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

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 实例。我添加了单例方法foo 作为示例。我不想覆盖 call - 这样做不会再调用该块。
  • 我添加了另一个示例,希望能阐明我想要实现的目标。
  • 在第二次尝试结束时,prc.callother_prc.call 都将返回 456。那是因为ident = other_proc 也改变了ident 对象prc 所指的对象。
  • @Stefan 是的,你是对的,当我说这个解决方案无法保存状态时,我的意思是这取决于调用者分配 ident 当前过程,当然,你的解决方案更优雅,但我喜欢它的简单性,并且是一种可以变得有用的模式(我在流的实现中使用了这种重新绑定技巧,而不是惰性枚举器,这就是我想到它的原因)。
【解决方案5】:

好的,现在我想我明白你的意思了。正如我在 cmets 中提到的,它可以通过嵌套闭包来完成。因为 Procs/lambdas 是匿名的,闭包嵌套为 lambda 提供了一种接收对自身的动态引用的方法,从而允许它在 self 的上下文中执行 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"

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

【讨论】:

  • 你基本上是从clone返回prc,也就是说,你实际上不再克隆了。在您的示例中,prcother_prc 是同一个对象:prc.equal?(other_prc) #=&gt; true
  • 好的,修好了。我不是只是clone 返回prc,我正在使用递归“再次构建相同的东西”来实现clone。 “克隆 Proc”通常没有意义——它的正常用途是复制值或对象(封装值)。
  • instance_eval 不需要,你可以写ret = Proc.new { ret.foo }。在 Ruby 中,proc 既是闭包又是实际对象(Proc 的实例)。或者,闭包可能包装在Proc 的实例中。我想要做的是从闭包中动态访问包含这个闭包的实例。覆盖clone 以创建一个“静态”解析为自身(或其foo)的新proc 使我的示例通过,但这不是我真正想要的。它或多或少是prc = Proc.new { prc.foo },后面跟着other_prc = Proc.new { other_prc.foo },只是隐藏在clone后面。
  • 嗯,我明白了。我认为使用嵌套的 Proc 来提供这种动态性应该是可能的——我稍后会试一试。否则你绝对不能这样做,因为应该在定义时设置 Proc 的范围,否则它不会是一个闭包。顺便说一句,这是一个相当学术的练习,因为我确信有更好的方法来实现您希望代码具有的任何最终外部行为(无论如何,它是什么?)
  • @Stefan 刚刚更新,这就是你要找的,对吧?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-29
  • 2011-05-31
  • 1970-01-01
  • 2011-04-08
  • 1970-01-01
  • 1970-01-01
  • 2022-06-21
相关资源
最近更新 更多