在 Ruby 中使用 String#to_proc 实现 to_s(2)

Implement to_s(2) with String#to_proc in Ruby

我正在学习一元运算符,&

关于在方法调用的参数中使用 & 有一些很好的问题。通常格式类似于 some_obj.some_method(&:symbol):

似乎主要思想是当一元运算符放在符号前面时,ruby 调用 :symbol 上的 to_proc 方法。因为 Symbol#to_proc 存在 "everything works".

我仍然对一切如何运作感到困惑。

如果我想实现一个 "to_proc sort of functionality with a string" 怎么办?我把它放在引号中是因为我什至不确定如何谈论我正在尝试做的事情。

但我们的目标是编写一个 String#to_proc 方法,以便以下工作:

class String
  def to_proc # some args?
    Proc.new do
      # some code?
    end
  end
end
p result = [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]

我是这样做的:

class String
  def to_proc
    Proc.new do |some_arg|
      parts = self.split(/ /)
      some_proc = parts.first.to_sym.to_proc
      another_arg = parts.last.to_i
      some_proc.call(some_arg, another_arg)
    end
  end
end
p result = [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]

我感到困惑的主要部分是如何将参数输入 String#to_proc 方法。好像是:

def to_proc
  Proc.new do |some_arg| ...
end

应该是:

def to_proc some_arg
  Proc.new do |yet_another_arg| ...
end

或类似的东西。 [2, 4, 6, 8] 值如何进入 String#to_proc returns 的过程?

就写这个

[2, 4, 6, 8].map { |each| each.to_s(2) }

虽然我猜这不是你要找的……

下面是Symbol#to_proc的实现方式。

class Symbol
  def to_proc
    proc { |each| each.send(self) }
  end
end

如果需要,您可以按如下方式在数组上定义 to_proc

class Array
  def to_proc
    symbol, *args = self
    proc { |each| each.send(symbol, *args) }
  end
end

然后使用

[2, 4, 6, 8].map(&[:to_s, 2])

另一种方法是使用 curry

尽管这不适用于绑定方法,因此您必须先定义一个 to_s lambda 函数。

to_s = lambda { |n, each| each.to_s(n) }

[2, 4, 6, 8].map(&to_s.curry[2])

尽管所有这些看起来更像是学术练习。

您的方法的问题在于,当参数始终作为字符串传递时,无法推断出参数的类型。

顺便回答一下你的问题:

How do the [2, 4, 6, 8] values get into the proc that String#to_proc returns?

这里是some_arg,这不是你必须定义的变量,而是调用proc时自动传递的参数。

下面是字符串补丁的重写和一些使用示例:

class String
  def to_proc
    fn, *args = split ' '
    ->(obj) { obj.send(fn.to_sym, *args) }
  end
end

这适用于以下示例:

p result = [[1,2,3]].map(&"join -")
# => ['1-2-3']

但是失败了(你的例子):

p result = [2, 4, 6, 8].map(&'to_s 2')
# => TypeError

问题是在调用 to_s('2') 时,2 应该是整数,而不是字符串。除了可能的一些序列化之外,我想不出任何方法来解决这个问题(尽管其他答案之一显示了 eval 如何工作)。

既然这种方法的局限性很明显,值得将它与更常用的 Symbol 补丁进行比较,以启用参数传递给 proc 速记(这取自 can-you-supply-arguments-to-the-mapmethod-syntax-in-ruby

class Symbol
  def call(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

a = [1,3,5,7,9]
a.map(&:+.(2))
# => [3, 5, 7, 9, 11] 

通过这种方式,您可以将任何类型的参数传递给 proc,而不仅仅是字符串。

定义后,您可以轻松地将字符串换成符号:

class String
  def call(*args, &blk)
    to_sym.call(*args, &blk)
  end
end

puts [1,2,3].map(&'+'.(1))

当你 运行 some_method(&some_obj), Ruby 首先调用 some_obj.to_proc 来获得一个过程,然后它 "converts" 那个过程到一个块并通过该块到 some_method。所以参数如何进入 proc 取决于 some_method 如何将参数传递给块。

例如,正如您定义的 String#to_proc,其中 returns 一个 proc{|arg| ...}(一个带有一个参数的过程),并调用 [...].map(&'to_s 2')、Ruby将其解释为

[...].map(&('to_s 2'.to_proc))

也就是

[...].map(&proc{|arg| ... })

最后

[...].map {|arg| ... }

重构代码

您可以自由选择 proc 块变量的名称。所以它可能是 yet_another_argsome_argsomething_else。在这种情况下,您传递给 to_proc 的对象实际上是您要从 self.

receive the proc call, so you could call it receiver. The method and param are in the String, so you get them with String#split 传递的对象
class String
  def to_proc
    proc do |receiver|
      method_name, param = self.split
      receiver.method(method_name.to_sym).call(param.to_i)
    end
  end
end

p result = [2, 4, 6, 8].map(&'to_s 2')
# => ["10", "100", "110", "1000"]

请注意,此方法已被定制为接受一个方法名称和一个整数参数。它在一般情况下不起作用。

另一种可能

警告

eval is evil

您已收到警告

这适用于您想要的确切语法,它也适用于范围更广的方法和参数:

class String
  def to_proc
    proc { |x| eval "#{x.inspect}.#{self}" }
  end
end

p [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]
p ["10", "100", "110", "1000"].map(&'to_i 2')
#=> [2, 4, 6, 8]
p [1, 2, 3, 4].map(&'odd?')
#=> [true, false, true, false]
p %w(a b c).map(&'*3')
#=> ["aaa", "bbb", "ccc"]
p [[1,2,3],[1,2],[1]].map(&'map(&"*2")')
#=> [[2, 4, 6], [2, 4], [2]]

不过,它也带来了安全问题。能力越大责任越大!