在Ceylon 简介 第8部分中,我们讨论了Ceylon对定义高阶函数的支持,特别是表示接受函数引用的参数类型的两种不同方式。以下声明在本质上等效
X[] filter<X>(X[] sequence, Callable<Boolean,X> by) { ... }
X[] filter<X>(X[] sequence, Boolean by(X x)) { ... }
我们甚至看到了如何将方法引用传递给这样的高阶函数
Boolean stringNonempty(String string) { return !string.empty; } String[] nonemptyStrings = filter(strings, stringNonempty);
当然,如果我们每次想要使用高阶函数时都必须声明一个整个方法,那么像filter()这样的通用高阶函数的许多便利性都会丧失。实际上,高阶函数的很大一部分吸引力在于能够通过具有更专用版本的传统控制结构(如for.
大多数支持高阶函数的语言都支持所谓的匿名函数(通常称为lambda 表达式),其中函数可以作为表达式的一部分内联定义。在一个类似C的语言中,我喜欢的语法如下
(String string) { return !string.empty; }
这是一个普通的方法声明,去掉了返回类型和名称。然后我们可以按照以下方式调用filter()如下
String[] nonemptyStrings = filter( strings, (String string) { return !string.empty; } );
由于匿名函数通常只包含一个表达式,因此我倾向于允许以下缩写
(String string) (!string.empty)
括号中的表达式被认为是方法的返回值。然后调用filter()会更简洁
String[] nonemptyStrings = filter(strings, (String string) (!string.empty));
这可以工作,并且我们可以在Ceylon语言中支持这种语法。
让我们看看如何使用匿名函数的更多示例
- 断言
assert ("x must be positive", () (x>0.0))
- 条件
when (x>100.0, () (100.0), () (x))
- 重复
repeat(n, () { writeLine("Hello"); })
- 制表
tabulateList(20, (Natural i) (i**3))
- 理解
from (people, (Person p) (p.name), (Person p) (p.age>18))
- 量化
forAll (people, (Person p) (p.age>18))
- 累积(折叠)
accumulate (items, 0.0, (Float sum, Item item) (sum+item.quantity*item.product.price))
问题是,我发现这些代码片段的可读性并不特别高。嵌套标点太多。它们显然没有内置控制结构(如for和if)的可读性好。对于多行匿名函数来说,问题会更严重。考虑
repeat (n, () { String greeting; if (exists name) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; } writeLine(greeting); });
肯定比a要丑得多!for循环!
在Smalltalk语言中,匿名函数真正派上用场——到了Smalltalk根本不需要任何内置控制结构。Smalltalk的独特之处在于其有趣的方法调用协议。方法参数按位置列出,类似于C或Java,但必须先于参数名称,并且不需要括号分隔。让我们将这个想法转化为Ceylon。
String[] nonemptyStrings = filter(strings) by (String string) (!string.empty);
注意,这里我们没有改变匿名函数的语法,只是将其移动到了括号外面。如果我们采用这种语法,我们可以使空参数列表成为可选的,而不引入任何语法歧义,允许以下这样的用法:
repeat (n) perform { String greeting; if (exists name) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; } writeLine(greeting); };
这看起来更像是一个内置的控制结构。现在让我们看看我们的其他一些例子。
- 断言
assert ("x must be positive") that (x>0.0)
- 条件
when (x>100.0) then (100.0) otherwise (x)
- 重复
repeat(n) perform { writeLine("Hello"); }
- 制表
tabulateList(20) containing (Natural i) (i**3)
- 理解
from (people) select (Person p) (p.name) where (Person p) (p.age>18)
- 量化
forAll (people) every (Person p) (p.age>18)
- 累积(折叠)
accumulate (items, 0.0) using (Float sum, Item item) (sum+item.quantity*item.product.price)
好吧,我不确定你是否也这样,但我发现所有这些例子都比之前的更容易阅读。事实上,我非常喜欢它们,以至于我不愿意支持更传统的“lambda表达式”风格。
另一方面,这种语法相当奇特,我确信很多人一开始会觉得难以阅读。
从理论上讲,我们没有任何理由不支持这两种变化,除了我们已经努力创建了一种具有一致风格的语言,其中通常有写某事的明显方式(显然,选择命名参数和位置参数是一个大例外,但我们有充分的理由)。问题是支持许多所谓的“无害”语法变化可能会使语言整体更难阅读,并导致一些令人烦恼的事情,比如:
- 编码规范
- 关于编码规范的争论
- 糟糕的工具来强制编码规范
- 关于使用哪种糟糕的工具来强制编码规范的争论
- 让那些更愿意争论编码规范和强制编码规范的工具的人受益,而不是那些想完成工作的人
因此,这是一个我们确实需要大量反馈的问题。我们应该支持:
- 传统的匿名函数吗?
- 只有像Smalltalk风格那样的匿名函数吗?
- 两者都支持?
答案对我们来说并不清晰。
更新:我意识到这篇文章是在邀请每个人提出他们自己最喜欢的匿名函数语法。我预计会看到所有这些类型的建议:
#{String string -> !string.empty}
function (String string) { return !string.empty; }
\String string -> !string.empty那很好,但请记住,我正在寻找的是:
- 与正常C风格函数声明非常一致的语法,
- 在语言的其余部分中不是不可能解析的。
你几乎可以自己发明或从其他语言复制任何东西,都可能会不符合这两个要求中的任何一个。
更新2:为了完整性,我应该提到,使用命名参数调用,你可以编写以下这样的代码:
String[] nonemptyStrings = filter { sequence = strings; local by(String string) { return !string.empty; } };
或者甚至
String[] nonemptyStrings = filter { sequence = strings; by = compose(not,String.empty); //compose()() is a higher order function that composes two functions };
我认为这种语法在某些地方是有意义的(例如,在用户界面定义中的回调),但对于我们一直在讨论的问题并不是理想的。