【问题标题】:Why don't I see pipe operators in most high-level languages?为什么我在大多数高级语言中看不到管道运算符?
【发布时间】:2010-12-07 21:29:03
【问题描述】:

在 Unix shell 编程中,pipe operator 是一个非常强大的工具。使用一小组核心实用程序、一种系统语言(如 C)和一种脚本语言(如 Python),您可以构建极其紧凑且功能强大的 shell 脚本,这些脚本由操作系统自动并行化。

显然,这是一个非常强大的编程范式,但除了 shell 脚本之外,我还没有将管道视为任何语言的一流抽象。使用管道复制脚本功能所需的代码似乎总是相当复杂。

所以我的问题是为什么我在 C#、Java 等现代高级语言中没有看到类似于 Unix 管道的东西?是否有支持一流管道的语言(除了 shell 脚本)?表达并发算法不是一种方便又安全的方式吗?

以防万一有人提出来,我看了一下F# pipe-forward 运算符(forward pipe operator),它看起来更像是一个函数应用程序运算符。据我所知,它将函数应用于数据,而不是将两个流连接在一起,但我愿意更正。

后记:在对实现协程进行一些研究时,我意识到存在某些相似之处。在blog postMartin Wolf 中描述了一个与我的问题类似的问题,但使用的是协程而不是管道。

【问题讨论】:

  • 我真的很想知道同样的事情,但从没想过要问。
  • FWIW 你可能对en.wikipedia.org/wiki/Hartmann_pipeline感兴趣
  • 请记住,F# 正向管道运算符应用函数的“数据”本身可以是一个函数,也可以是一个函数序列。
  • 这也让我大吃一惊,很少有语言能做到这一点。 Java 中的链式迭代器是一个 PITA。
  • F# 中有|>,R 中有%>%

标签: language-agnostic programming-languages concurrency shell pipe


【解决方案1】:

哈哈!感谢我的 Google-fu,我找到了您可能感兴趣的 an SO answer。基本上,答案是通过重载按位或运算符以提供类似 shell 的管道,从而违背“不要重载运算符,除非你真的必须”的论点,从而导致 Python 代码如下:

for i in xrange(2,100) | sieve(2) | sieve(3) | sieve(5) | sieve(7):
    print i

从概念上讲,它的作用是将数字列表从 2 到 99 (xrange(2, 100)) 通过一个筛子函数删除给定数字的倍数(先是 2,然后是 3,然后是 5,然后是 7)。这是质数生成器的开始,尽管以这种方式生成质数是一个相当糟糕的主意。但我们可以做得更多:

for i in xrange(2,100) | strify() | startswith(5):
    print i

这会生成范围,然后将它们全部从数字转换为字符串,然后过滤掉不以 5 开头的任何内容。

这篇文章展示了一个基本的父类,它允许您重载两个方法,mapfilter,来描述管道的行为。所以strify() 使用map 方法将所有内容转换为字符串,而sieve() 使用filter 方法清除不是数字倍数的内容。

它非常聪明,虽然这可能意味着它不是非常 Pythonic,但它展示了你所追求的东西以及一种可能很容易应用于其他语言的技术。

【讨论】:

  • 不错。我当然投了赞成票,但我的疑虑是没有一个函数是并行执行的。我真正想要的是更像 Erlang 和 Java 的答案。
  • @cdiggins - 你的疑虑是一个实现细节。同样的功能可以很容易地在 C(甚至是汇编?)中实现以利用并行性,并且因为所有粗暴的工作都是在基类中完成的,所以用纯 Python 编写的派生类仍然可以获得这种功能(这两种方法你必须重载 - mapfilter - 一次只能处理一个元素)。当然,如果我们愿意实现它,我们可以对大多数常规函数结构做同样的事情,不管有没有 Unix-pipe 接口。
  • 实际上我对你不能使用管道字符本身并不感到惊讶,但我有点失望的是在某种程度上没有简单的“并发数据管道”可用。像concat_file(myfile) |> filter("mypattern") |> write_file(myoutfile)(或类似的东西)这样的东西可以工作,对吧?基本上它会创建 3 个线程并从一个线程并行发送数据。
【解决方案2】:

您可以在 Erlang 中轻松实现流水线类型的并行性。以下是我 2008 年 1 月博文的无耻复制/粘贴。

另外,Glasgow Parallel Haskell 允许并行函数组合,这相当于同一件事,为您提供隐式并行化。

你已经在考虑 管道 - “gzcat”怎么样 foo.tar.gz | tar xf -"?你可能不会 已经知道了,但外壳是 运行解压缩和解压缩 并行 - 在 tar 中读取的标准输入只是 阻塞,直到数据被发送到标准输出 gzcat.

嗯,可以表达很多任务 在管道方面,如果可以的话 这样做然后获得一定程度的 David 的并行化很简单 King 的助手代码(甚至跨 erlang 节点,即机器):

pipeline:run([pipeline:generator(BigList),
          {filter,fun some_filter/1},
          {map,fun_some_map/1},
          {generic,fun some_complex_function/2},
          fun some_more_complicated_function/1,
          fun pipeline:collect/1]).

所以基本上他在这里所做的是 列出步骤 - 每一步 以一种有趣的方式实施 接受之前的任何输入 步骤输出(乐趣甚至可以是 当然是内联定义的)。去检查 出大卫的blog entry 代码和更详细的解释。

【讨论】:

  • 这非常符合我的想法。明天喝杯咖啡后,我需要再学习一下!
  • 我接受了这个答案,因为它是我看到的最优雅的解决方案,Mark 在上面的评论中向我指出了 Hartmann 管道,这使我开始使用基于流的编程。
  • map-reduce 是管道的一个例子吗?我真的很喜欢 JavaScript 的内置函数。但我也很失望,即使 golang 没有像 bash 这样的特殊语法
【解决方案3】:

magrittr package 提供类似于 F# 在 R 中的管道转发运算符:

rnorm(100) %>% abs %>% mean

结合dplyr包,它带来了一个简洁的数据操作工具:

iris %>%
  filter(Species == "virginica") %>%
  select(-Species) %>%
  colMeans

【讨论】:

    【解决方案4】:

    您可以在 C# 和 Java 中找到类似管道的东西,例如,您可以在其中获取一个连接流并将其放入另一个连接流的构造函数中。

    所以,你有 Java:

    new BufferedReader(new InputStreamReader(System.in));
    

    您可能想要查找链接输入流或输出流。

    【讨论】:

    • 我更多地考虑并行运行的两个独立进程,一个吐出数据,另一个消耗数据。
    • 那么你只需要使用 PipedInput (Output) Stream:java.sun.com/javase/6/docs/api/java/io/PipedInputStream.html,但这不是并行的,但你可以在通过流时转换数据。
    • @James,PipedInputStream 和 PipedOutStream 非常接近我想要的。我想知道语言中对这个东西(具有自动并行性)的一流支持。原来,我正在寻找的是所谓的“流编程”。非常感谢你向我展示了一些很酷的 Java 东西。
    • 这种方法的问题是你必须“向后”思考:阅读代码时,首先阅读最后一个函数,并以第一个应用函数结束。 IE。与 shell 的管道和操作顺序相比,顺序是相反的。我更喜欢System.in | (new InputStreamReader) | (new BufferedReader)
    • @jrouquie - 在函数式编程类型语言中,这现在更容易做到,例如,使用 F# 或 Scala 来获得你想要的行为。
    【解决方案5】:

    感谢所有出色的答案和 cmets,这里是我学到的总结:

    事实证明,有一个与我感兴趣的东西相关的完整范式,称为Flow-based programming。专门为基于流的编程设计的语言的一个很好的例子是Hartmann pipelines。 Hartamnn 管道概括了 Unix 和其他操作系统中使用的流和管道的概念,以允许多个输入和输出流(而不仅仅是单个输入流和两个输出流)。 Erlang 包含强大的抽象,可以轻松地以类似于管道的方式表达并发进程。 Java 提供了PipedInputStreamPipedOutputStream,它们可以与线程一起使用,以更详细的方式实现相同的抽象。

    【讨论】:

    • 感谢您发布问题。我一直在想同样的事情,但由于恶劣的环境堆栈溢出而不敢问
    【解决方案6】:

    我认为最根本的原因是因为 C# 和 Java 往往被用来构建更多的单体系统。从文化上讲,甚至想做类似管道的事情并不常见——您只需让您的应用程序实现必要的功能。构建大量简单工具然后以任意方式将它们粘合在一起的想法在这些情况下并不常见。

    如果您查看一些脚本语言,例如 Python 和 Ruby,就会发现有一些非常好的工具可以在这些脚本中执行类似管道的操作。例如,查看 Python 子流程模块,它允许您执行以下操作:

    proc = subprocess.Popen('cat -',
                           shell=True,
                           stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE,)
    stdout_value = proc.communicate('through stdin to stdout')[0]
    print '\tpass through:', stdout_value
    

    【讨论】:

    • 这不只是从 Python 驱动 Shell 的管道吗?据我了解,OP 的问题是关于在语言内部有一个类似管道的结构。
    • 有趣,我认为所有管道都只是像线程一样包裹 os 管道,但现在你提到它,我想知道它们是什么时候,它们不是它们自己的范式实现。我想我们不应该重新发明轮子,但这是我可能没有深入了解的核心内容。我只知道我喜欢管道
    【解决方案7】:

    你在看 F# |> 运算符吗?我认为您实际上想要 >> 运算符。

    【讨论】:

      【解决方案8】:

      通常你不需要它,没有它程序运行得更快。

      基本上管道是消费者/生产者模式。而且编写这些消费者和生产者并不难,因为他们不共享太多数据。

      • Python 管道:pypes
      • Mozart-OZ 可以使用端口和线程来做管道。

      【讨论】:

      • Pypes,非常棒,跟我想的差不多。
      【解决方案9】:

      Objective-C 有 NSPipe 类。我经常使用它。

      【讨论】:

        【解决方案10】:

        我在 Python 中构建管道函数时获得了很多乐趣。我有一个我写的库,我把内容和一个示例运行here。最适合我的是 XML 处理,在 Wikipedia article 中进行了描述。

        【讨论】:

          【解决方案11】:

          您可以通过链接/过滤/转换迭代器在 Java 中执行类似管道的操作。 你可以使用谷歌的Guava Iterators

          我会说,即使有非常有用的 guava 库和静态导入,它最终仍然是大量的 Java 代码。

          在 Scala 中,制作自己的管道运算符非常容易。

          【讨论】:

          • 多年来我一直在使用它们,但没有意识到它们可以帮助并发!
          【解决方案12】:

          基于协程的流式库已经在 Haskell 中存在了相当长的一段时间。两个流行的例子是conduitpipes

          这两个库都写得很好,文档也很完善,而且都比较成熟。 Yesod web 框架是基于管道的,它是pretty damn fast。 Yesod 在性能上可以与 Node 竞争,甚至在一些地方击败它。

          有趣的是,所有这些库默认都是单线程的。这是因为管道的唯一激励用例是受 I/O 限制的服务器。

          【讨论】:

            【解决方案13】:

            自从今天 R 加入了管道操作符,值得一提的是 Julialang 一直有管道:

            help?> |>
            search: |>
            
              |>(x, f)
            
            
              Applies a function to the preceding argument. This allows for easy function chaining.
            
              Examples
              ≡≡≡≡≡≡≡≡≡≡
            
              julia> [1:5;] |> x->x.^2 |> sum |> inv
              0.01818181818181818
            

            【讨论】:

            • 哇,R 是最近才引入这个的吗?
            【解决方案14】:

            如果您仍然对答案感兴趣...

            您可以查看因素,或旧的快乐和来往的连接范式。 in 参数和 out 参数是隐式的,转储到堆栈中。然后下一个词(函数)获取该数据并对其进行处理。

            语法是后缀。

            “123”打印

            其中 print 接受一个参数,无论堆栈中的内容是什么。

            【讨论】:

              【解决方案15】:

              您可以在 python 中使用我的库:github.com/sspipe/sspipe

              【讨论】:

                猜你喜欢
                • 2019-07-16
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-06-04
                • 1970-01-01
                • 1970-01-01
                • 2020-11-17
                • 1970-01-01
                相关资源
                最近更新 更多