7

各种编程语言中的 Lambda

 8 months ago
source link: https://www.techug.com/post/lambda-lambda-lambda/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

各种编程语言中的 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK