各种编程语言中的 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:
- 完全匿名函数(这些 lambdas 接受某种参数列表,然后有一个独立的主体,如 C++ 或 Java 或……)
- 占位符表达式(带有特殊占位符的单一表达式,如 Scala、Clojure 或 Boost.Lambda 或…)。在这方面,Swift 似乎是独一无二的,它使用
$0
作为第一个参数,而我熟悉的所有其他语言和库都是从1
开始计数。 - Partial 函数应用(从技术上讲实际上不是 lambdas,但解决了相同的问题,所以足够接近,如 Haskell 或 std::bind)
有几种语言在这里也有多个选项。
值得注意的是,C++ 的 lambda 几乎是最长的 lambda。这有点令人惊讶, C++ 的 lambda 之所以长,是因为 C++ 的基本复杂性——Go 的借口是什么?所以这很酷。技术上不是最后一个!
不过,这对 C++ 来说是一个有利的比较,因为我们都在获取一个值并返回一个值。如果我们需要获取一个引用,那就需要使用 auto const&
或 auto&&
(长 7 或 2 个字符)。如果我们想返回一个引用而不是一个值呢?那就使用 -> decltype(auto)
,这样就多了 17 个字符,和其他 lambda 一样长。
C++ 的 lambdas 有三个部分在这套语言中是独一无二的,或者说大部分是独一无二的:
- 指定捕获。例如,Rust 允许通过move来捕获,写成
move |needle|haystack.contains(needle)
。正如用户 Nobody_1707 在 reddit 上指出的,Swift 也有与 C++ 相当类似的捕获。但除此之外,我不确定其他语言是否有捕获的概念。基本上就是[&]
。话虽如此,鉴于 C++ 没有垃圾回收,我不确定除了[]
之外还有什么好的捕获默认设置,而在这一点上,我们并不能节省很多字符。 - 强制性参数声明。在许多其他静态类型语言中,你可以提供类型注解,但它是可选的。在 Rust 中,例子可以是
|e: i32| e < 0
,就像在 Scala 中,可以是(e: Int) => e < 0
。在简单的情况下,你可能会避免使用类型,而在更复杂的情况下,你可能更愿意保留它。 - 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
你也许感兴趣的:
- 【程序员搞笑图片】数据类型简明指导
- 33 种编程语言的 UUIDv7 实现
- 【外评】Rust,你错了
- 【外评】为什么人们对 Go 1.23 的迭代器设计感到愤怒?
- 华为自研编程语言“仓颉”来了!鸿蒙应用开发新语言,性能优于 Java、Go、Swift
- 【外评】JavaScript 变得很好
- 【外评】华为发布自己的编程语言 “仓颉”
- VBScript 废弃:时间表和后续步骤
- 【外评】BASIC 编程语言 60 岁了
- 【外评】为什么 ALGOL 是一种重要的编程语言?
你对本文的反应是: