Ruby 中类似 Elixir 的管道来处理集合

Posted

技术标签:

【中文标题】Ruby 中类似 Elixir 的管道来处理集合【英文标题】:Elixir-like pipes in Ruby to process collections 【发布时间】:2022-01-02 18:10:26 【问题描述】:

在 Elixir 中有一个很棒的管道操作员是这样工作的:

"hello, world!"
  |> String.split(" ")
  |> Enum.map(&String.capitalize/1)
  |> Enum.join

在 Ruby 中我们可以使用类似的语法:

"hello, world!"
  .split(" ")
  .map(&:capitalize)
  .join

只有在为对象本身定义了所有这些方法时才有效。如果需要调用一些本地方法,我们应该使用类似:

.map  |el| URI.parse(el) 

但是如果我们想做一些集合处理(不是单个元素),例如 GZIP 压缩:

chars = text
  .downcase
  .chars

compressed = GZipped.new(chars).bytes

但是链条断了!

我找到了一些链接,但看起来不太好:

pipe_envy - 丑陋!没有收藏 chainable_methods - 没有收藏 How to use chainable_methods piperator - 好多了!但看起来很重

在我看来,有这样的东西会很棒:

text
  .split
  .pipe(URI.method(:parse))
  .map(&:to_s)
  .join
  .pipe(GZIPped)
  .pipe(Base64.method(:encode))

在 Ruby 中构建此类管道的最佳方法是什么?

更新 1

这是一个例子

class Dedup
  def initialize(obj)
    @obj = obj
  end

  def each
    Enumerator.new do |y|
      prev = nil

      @obj.each do |el|
        if el != prev
          y << el
          prev = el
        end
      end
    end
  end
end


expect(
  "1 1 1 2 2 3"
    .split
    .then  |obj| Dedup.new(obj).each 
    .to_a
).to eq [1, 2, 3]

这种链接看起来丑陋且难以阅读。

比较:

expect(
  "1 1 1 2 2 3"
    .split
    .pipe(Dedup)
    .to_a
).to eq [1, 2, 3]

【问题讨论】:

根据我的经验,试图让一种语言模仿另一种语言很少奏效。我宁愿以惯用代码为目标,使用 Ruby 提供的语言工具。 @Stefan 你能提供一些 idiomatic 的 Ruby 风格的代码吗? 在elixir 中也不鼓励为了管道而进行管道。声明一个局部变量 chars 并在其上调用 GZipped.new(chars)。除非你在愚弄队友之后,否则长管道没有什么好处。 @SergeiO.Udalov 示例代码似乎很做作。你有一个实际的问题想要解决吗? 是的。真实案例是将elixir代码移植到ruby。是的,我们可以用 ruby​​ 方式重写它,但最好保持尽可能接近。 【参考方案1】:

已经有这样的方法,至少从 Ruby 2.5 开始 - yield_self,在 Ruby 2.6 中别名为 then。您可以将&amp; 运算符与响应to_proc 的任何对象一起使用来传递它而不是块。

text
  .split
  .map(&URI.method(:parse)) # URI#parse expects a string, not an array
  .map(&:to_s)
  .join
  .then(&GZIPped) # not sure what GZIPped is - I'll assume it has .to_proc method
  .then(&Base64.method(:encode))

(我可能应该提到上面的代码实际上不会工作,老实说,我不知道它会做什么 - 为什么要拆分字符串,将它们转换为 url,然后再返回到字符串?唯一的事情这是为了提高 id 其中一个 substrngs 不是有效字符串?但是然后您尝试将结果字符串读取为 gzip 压缩文件...我假设我误解了您的代码中的某些内容)

更高级的东西——我非常喜欢 elixir 中的一件事,就是可以将方法与剩余的参数链接在一起。这也可以在 ruby​​ 中进行模拟,但需要一些工作并仔细考虑是否值得:

module MyMath
  module_function
  
  UNDEFINED = Object.new

  def add(a, b = UNDEFINED)
    if b == UNDEFINED
      return ->(num)  add(a, num) 
    end
    a + b
  end
end

MyMath.add(2,5) #=> 7
[1,2,5,9].map(&MyMath.add(5)] #=> [6,7,10,14]

【讨论】:

总有.then |obj| ... 允许任意转换。 @Stefan @BroiSatse 你能看看我提供的“更新 1”吗?无法理解如何使用 .then 使其可读 @SergeiO.Udalov - 老实说,这在 ruby​​ 中将是一件非常难以理解的事情 - 您正在尝试混合功能和面向对象的编程。重复数据删除不是功能。 Buttttt,您可以定义返回-&gt;(obj) self.new(obj).each self.to_proc 方法,您可以使用then(&amp;Dedup)。但是,这很奇怪 真实案例是将elixir代码移植到ruby。是的,我们可以用 ruby​​ 方式重写它,但最好让它尽可能接近。 self.to_proc 为我工作!非常感谢,伙计! 次要注释MyMath#add 是一个实例方法,因此无法作为MyMath.add() 访问,您需要将方法定义更改为self.add 或添加module_function :add

以上是关于Ruby 中类似 Elixir 的管道来处理集合的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Elixir 中不按 Enter 键获取单个字符?

Pipeline in scala——给scala添加管道操作

java 8 stream学习整理

在 Ruby 中处理多个进程

如何在 Windows 中拥有最新版本的 Elixir

Ruby操作MongoDB(进阶八)-聚合操作Aggregation