各种编程语言中的 Lambda

我喜欢看 Conor Hoekstra 的视频,一方面是因为他是一位引人入胜的主持人,另一方面是因为他介绍了很多很多编程语言。我不懂那么多语言,所以能接触到不同语言如何解决相同的问题是件好事。

他最近的一段视频讨论了一个相当简单的问题(如何计算矩阵中负数的个数),他用了十几种语言来实现这个问题。所有语言都必须有某种机制来确定一个数字是否为负数–对于大多数语言来说,这涉及到使用 lambda(有时称为匿名函数)。

我发现特别有趣的是所有不同语言的 lambda 看起来都是怎样的,因此我想特别关注这一点。在特定的语言中,你会如何编写一个 lambda 表达式来检查给定的数字是否为负数?

(<0)                              // 4:  Haskell
_ < 0                             // 5:  Scala
_1 < 0                            // 6:  Boost.Lambda
#(< % 0)                          // 8:  Clojure
&(&1 < 0)                         // 9:  Elixir
|e| e < 0                         // 9:  Rust
\(e) e < 0                        // 10: R 4.1
{ $0 < 0 }                        // 10: Swift
{ it < 0 }                        // 10: Kotlin
e -> e < 0                        // 10: Java
e => e < 0                        // 10: C#, JS, Scala
\e -> e < 0                       // 11: Haskell
{ |e| e < 0 }                     // 13: Ruby
{ e in e < 0 }                    // 14: Swift
{ e -> e < 0 }                    // 14: Kotlin
fun e -> e < 0                    // 14: F#, OCaml
lambda e: e < 0                   // 15: Python
(λ (x) (< x 0))                   // 15: Racket
fn x -> x < 0 end                 // 17: Elixir
(lambda (x) (< x 0))              // 20: Racket/Scheme/LISP
[](auto e) { return e < 0; }      // 28: C++
std::bind(std::less{}, _1, 0)     // 29: C++
func(e int) bool { return e < 0 } // 33: Go

对于需要使用大括号的语言,我会计算大括号的数量(无论这是否公平)。另外,Clojure 可以使用 %1 代替 %

是的,请注意,Boost.Lambda std::bind 也在该列表中(并假设您在占位符的作用域中使用using namespace声明)。

我认为这张表本身就很有趣。它基本上说明了这里确实有三种 lambda:

  1. 完全匿名函数(这些 lambdas 接受某种参数列表,然后有一个独立的主体,如 C++ 或 Java 或……)
  2. 占位符表达式(带有特殊占位符的单一表达式,如 Scala、Clojure 或 Boost.Lambda 或…)。在这方面,Swift 似乎是独一无二的,它使用 $0 作为第一个参数,而我熟悉的所有其他语言和库都是从 1 开始计数。
  3. Partial 函数应用(从技术上讲实际上不是 lambdas,但解决了相同的问题,所以足够接近,如 Haskell 或 std::bind)

有几种语言在这里也有多个选项。

值得注意的是,C++ 的 lambda 几乎是最长的 lambda。这有点令人惊讶, C++ 的 lambda 之所以长,是因为 C++ 的基本复杂性——Go 的借口是什么?所以这很酷。技术上不是最后一个!

不过,这对 C++ 来说是一个有利的比较,因为我们都在获取一个值并返回一个值。如果我们需要获取一个引用,那就需要使用 auto const&auto&&(长 7 或 2 个字符)。如果我们想返回一个引用而不是一个值呢?那就使用 -> decltype(auto),这样就多了 17 个字符,和其他 lambda 一样长。

C++ 的 lambdas 有三个部分在这套语言中是独一无二的,或者说大部分是独一无二的:

  1. 指定捕获。例如,Rust 允许通过move来捕获,写成 move |needle|haystack.contains(needle)。正如用户 Nobody_1707 在 reddit 上指出的,Swift 也有与 C++ 相当类似的捕获。但除此之外,我不确定其他语言是否有捕获的概念。基本上就是 [&]。话虽如此,鉴于 C++ 没有垃圾回收,我不确定除了 [] 之外还有什么好的捕获默认设置,而在这一点上,我们并不能节省很多字符。
  2. 强制性参数声明。在许多其他静态类型语言中,你可以提供类型注解,但它是可选的。在 Rust 中,例子可以是 |e: i32| e < 0,就像在 Scala 中,可以是 (e: Int) => e < 0。在简单的情况下,你可能会避免使用类型,而在更复杂的情况下,你可能更愿意保留它。
  3. return 关键字。在其他语言中,我们只有一个表达式。

我曾试图提出的一个建议(P0573)可以创建一种新形式的 “完全匿名函数”,使参数声明成为可选项,并省略返回关键字。该文件建议:

[](e) => e < 0                // P0573R2: 14
[](auto e) { return e < 0; }  // C++: 28

这样长度就减少了一半。虽然还是比其他大多数语言长,但已经好很多了。然而,这个提议由于一些显著的原因被否决了:不同的解析问题(人类对未命名参数的歧义)和返回类型的含义。请参阅我之前关于该主题的文章。我认为取消类型注释对 C++ 来说之所以困难,部分原因在于我们的参数声明是 Type name 形式,而许多其他语言则将其写成 name: Type – 后者更适合省略类型并将重点放在名称上(而且不允许使用未命名参数,这是 C++ 问题的关键所在,反正我从来没觉得这是一个特别重要的特性)。

因此,我认为 “完全匿名函数 “的新语法可能不会被提出来–我不知道如何用类似 C++ lambdas 的语法来克服这两个问题(尽管对于在Prague 提出的第三个问题,P2036 得到了很好的回应,而且似乎有可能作为缺陷被接受)。在我看来,为完整的 lambdas 引入不同的语法目前并不可取(无论如何,仍然会有 auto decltype(auto) 的问题)。

但这样一来,占位符表达式的问题就悬而未决了。我原本有些嘲笑这种样式,认为它比完整的匿名函数样式更难读。但对于简单的情况,我不再那么肯定了。正如 vector<bool> 在 Now I Am Become Perl 中指出的,最初的困惑和永久的困惑是不同的(在此之前,他还提出了一些语法建议,这些建议肯定会引起最初的困惑)。但他指出的正是表达式 lambda 占位符的概念。

这种语法可能是什么样的呢?我们仍然希望保留 “捕获 “的概念–我认为这仍然是 C++ 中的一个重要概念,而且无论如何我们都需要一个引入者。这样做的原因是,考虑一下:f(_1)。这意味着什么?

f([](auto&& x, auto&&...) -> decltype(auto) { return (x); })

[](auto&& x, auto&&...) -> decltype(auto) { return f(x); }

如果这取决于背景……那么,你如何决定?这似乎是个难题。老实说,我并不完全确定 Scala 是怎么做的。Clojure、Elixir 和 Swift 对于 lambda 的起始位置都有明确的标记。而且我不认为我们真的可以在这里使用大括号–比如 { f(_1) }

也许我们可以使用引入符,然后是某种标点符号,最后是表达式?

 

[] => _1 < 0
[] -> _1 < 0
[]: _1 < 0

这当然有所不同,但基本上与 Boost.Lambda 中的功能相同(只是可能产生更好的代码)。

在 HOPL 论文中有一个演示 STL.Lambda 的例子:

vector<string>::iterator p =
    find_if(v.begin(), v.end(), Less_than<string>("falcon"));

考虑一下这里函数对象的形状–这是一个partial 函数应用。这正是我们在 Haskell 中会写的 (< "falcon")(无论如何,从语义上讲)。比约恩-法勒(Björn Fahller)有一个完整的资源库,其中的函数对象都支持类似的partial 函数应用,唯一不同的是,他的版本去掉了类型:less_than("falcon")

现在,与之对应的 C++ lambda 会是什么呢?

 

[](std::string const& s) { return s < "f"; } // 44: C++11
[](auto&& s) { return s < "f"; }             // 32: C++14
lift::less_than("f")                         // 20: with lift 

这就是为什么我经常编写泛型 lambda,即使我只需要单态的 lambda。我不得不缩短字符串,因为 lambda 太宽了,我的博客放不下!谢谢你,费萨尔!

如果这种风格对两个编程风格迥异的人(尽管他们的名字中大部分字母都相同)来说已经足够好了,那么也许参数名无论如何都被高估了?我的意思是,它确实读起来很不错:find_if(..., less_than(...)) 是很不错的英文。如果我们用运算符代替单词,真的会有什么不同吗?

(<"f")                           // 6: Haskell
[]: _1 < "f"                     // 12: placeholder?
lift::less_than("f")             // 20: with lift 
[](auto&& s) { return s < "f"; } // 32: C++14

我可以习惯这一点。我并不觉得这会造成永久性的困惑。

当然,作为 C++,还有很多其他问题需要考虑。比如,如何处理转发(使用宏),如何处理可变参数(我不知道),这些 lambdas 的 arity 是什么,是基于存在的最大占位符吗(不是),还是需要 P0834 来处理这个问题(问得好),或者像 P0119 中建议的那样,使用更简短的形式来指定操作符函数(是的,特别是 (>) 语法,作为 std::greater() 的更简短写法)。

不过,这已经完全偏离了本篇文章的主旨,那就是:C++ 的 lamb 长度真的非常非常长:C++ 的 lambdas 真的非常非常长。

本文文字及图片出自 Lambda Lambda Lambda

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注