为什么大多数编程语言都具有用于声明函数的特殊关键字或语法?[关闭]


39

大多数编程语言(动态和静态类型的语言)都具有特殊的关键字和/或语法,这些关键字和/或语法与用于函数声明的变量大不相同。我认为函数就像声明另一个命名实体一样:

例如在Python中:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

为什么不:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

同样,使用Java之类的语言:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

为什么不:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

这种语法似乎是更自然的方式来声明某些内容(无论是函数还是变量)和少一个类似deffunction使用某些语言的关键字。而且,IMO更一致(我在同一地方查看以了解变量或函数的类型),并且可能使解析器/语法的编写更加简单。

我知道很少有语言使用这种想法(CoffeeScript,Haskell),但是大多数常见语言对函数(Java,C ++,Python,JavaScript,C#,PHP,Ruby)都有特殊的语法。

即使在同时支持两种方式(并具有类型推断)的Scala中,也更常见的是编写:

def addOne(x: Int) = x + 1

而不是:

val addOne = (x: Int) => x + 1

IMO,至少在Scala中,这可能是最容易理解的版本,但是很少遵循以下惯用法:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

我正在开发自己的玩具语言,我想知道如果我以这种方式设计语言是否有任何陷阱,并且是否有任何历史或技术原因未广泛遵循这种模式?


9
原因可能是历史原因。谁先这样做是谁,其他人都照搬了。但我怀疑我们能否确定。

29
我认为这是因为函数或方法不只是另一个命名实体。在函数式语言中,它们是(可以将它们传递给其他对象),但是在其他语言(例如Java)中,函数或方法与简单变量完全不同,因此不能被这样对待(我承认Java 8弱化了这一点)语句),因此以不同的方式定义函数/方法是有意义的,因为它们的行为方式不同。
2014年

15
我不同意您的建议是一种更自然的声明功能的方式。我真的不喜欢Coffeescript的语法,部分原因在于它的函数声明语法。事实是,如果它没有损坏,请不要修复它。与一些花哨的Perl式符号相比,写“ def”或“ function”没什么大不了,而且乍一看也很明显,这些符号如果不加思索或疲倦的眼睛就很容易被误认为是其他东西。完全个人而言,我还认为示例中建议的语法比当前语法难看得多。
罗伊

22
“ =>”如何不是“ 用于声明函数的特殊关键字或语法 ”?
TML

11
我不了解您,但对我(int => int) addOne = (x) => {来说,比int addOne(int) {…… 更“特殊”和“复杂”
Bogdan Alexandru

Answers:


48

我认为原因是大多数流行语言都来自C语言家族,或者受其影响,而不是功能语言及其根源lambda演算。

而在这些语言中,函数是不是只是一个值:

  • 在C ++,C#和Java中,您可以重载函数:您可以具有两个名称相同但签名不同的函数。
  • 在C,C ++,C#和Java中,可以具有表示函数的值,但是函数指针,函子,委托和函数接口均不同于函数本身。部分原因是大多数这些函数实际上并非仅仅是函数,它们是具有某些(可变)状态的函数。
  • 变量默认情况下(你必须使用可变的constreadonlyfinal禁止突变),但功能不能被重新分配。
  • 从技术角度来看,代码(由功能组成)和数据是分开的。它们通常占用内存的不同部分,并且以不同的方式访问它们:一次加载代码,然后仅执行(但不能读取或写入),而数据经常被不断分配和释放,并且正在被写入和读取,但从未执行。

    而且由于C的意思是“接近金属”,因此在语言的语法中也应反映出这种区别。

  • 形成函数式编程基础的“函数就是价值”方法只是在相对较近的时候才在通用语言中获得关注,这一点已在C ++,C#和Java中较晚引入lambda得以证明(2011、2007、2014)。


12
自2005
Eric Lippert 2014年

2
“从更技术的角度来看,代码(由功能组成)和数据是分开的” –某些语言,最众所周知的是LISP不会进行这种分开,并且对代码和数据的对待也没有太大的区别。(LISP是最著名的示例,但是还有很多其他语言,例如REBOL可以做到这一点)
Benjamin Gruenbaum 2014年

1
@BenjaminGruenbaum我在谈论内存中的编译代码,而不是语言级别。
svick 2014年

C具有至少至少可以视为另一个值的函数指针。可以肯定,这是一个危险的值,但奇怪的事情已经发生了。
Patrick Hughes 2014年

@PatrickHughes是的,我在第二点提到他们。但是函数指针与函数完全不同。
2014年

59

这是因为对于人类而言,重要的是要认识到功能不仅是“另一个命名实体”。有时,按原样操作它们很有意义,但是一眼就能识别它们。

计算机对语法的看法实际上并不重要,因为难以理解的字符斑点对于机器解释来说是很好的,但是对于人类的理解和维护来说这几乎是不可能的。

这实际上与为什么我们有while和for循环,切换以及其他(如果有)等等的原因相同,即使它们最终都归结为一个比较和跳转指令。原因是因为这样做是为了维护和理解代码。

按照提议的方式将功能用作“另一个命名实体”,这会使您的代码更难于查看,因此也更难理解。


12
我不同意将函数视为命名实体必然会使代码难以理解。对于遵循过程范式的任何代码,几乎可以肯定地如此,但是对于遵循功能范式的代码,则可能并非如此。
凯尔·斯特兰德

29
“这确实是与为什么我们有while和for循环,切换以及其他情况(等等)的原因相同,即使所有这些最终最终归结为一个比较和跳转指令。 ” +1。如果所有程序员都对机器语言感到满意,我们将不需要所有这些花哨的编程语言(高级或低级)。但事实是:每个人对于要编写代码的机器的距离有不同的舒适度。找到级别后,您只需要遵守所提供的语法即可(或者,如果您真的很烦,请编写自己的语言规范和编译器)。
Hoki 2014年

12
+1。更为简洁的版本可能是“为什么所有自然语言都将名词与动词区分开?”
msw

2
“一个难以理解的字符斑点对于机器来说是很好的解释,但是对于人类来说,理解和维护它几乎是不可能的。”对于问题中提出的这种适度的句法变化,这简直是轻率的资格。一种快速适应语法的方法。与OO方法调用语法object.method(args)当时相比,该提议与约定的根本性差异要小得多,但是尚未证明后者对于人类的理解和维护几乎是不可能的。尽管在大多数OO语言中都不应将方法视为存储在类实例中。
Marc van Leeuwen 2014年

4
@MarcvanLeeuwen。您的评论是正确且合理的,但删除原始帖子的方式会引起一些困惑。实际上有两个问题:(i)为什么这样?(ii)为什么不这样呢?。答案whatsisname是更着重于第一点(并警告您删除这些故障保险的危险),而您的评论与问题的第二部分更相关。确实有可能更改此语法(并且正如您所描述的,它已经进行了很多次...),但它并不适合所有人(因为oop也不适合所有人。)
Hoki 2014年

10

您可能有兴趣了解一下,追溯到史前时期,一种名为ALGOL 68的语言所使用的语法与您所建议的相近。认识到函数标识符与其他标识符一样绑定到值,您可以使用该语言使用以下语法声明一个函数(常量)

函数类型 名称 =(参数列表结果类型主体 ;

具体来说,您的示例将显示为

PROC (INT)INT add one = (INT n) INT: n+1;

认识到冗余,因为可以从声明的RHS中读取初始类型,并且由于函数类型始终以开头PROC,所以可以(通常是)将其压缩为

PROC add one = (INT n) INT: n+1;

但请注意,=仍然参数列表之前。还要注意,如果您想要一个函数变量(稍后可以向其分配相同函数类型的另一个值),=则应将替换为:=,从而可以选择

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

但是,在这种情况下,两种形式实际上都是缩写。由于标识符func var指定了对本地生成的函数的引用,因此完全扩展的形式为

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

这种特殊的语法形式很容易习惯,但是显然在其他编程语言中并没有那么多的追随者。比如Haskell甚至函数式编程语言更喜欢风格f n = n+1= 下面的参数表。我想原因主要是心理上的。毕竟,即使数学家也不常常喜欢,像我一样,˚F = ññ + 1在˚Fñ)= ñ + 1。

顺便说一句,上面的讨论确实突出了变量和函数之间的一个重要区别:函数定义通常将名称绑定到一个特定的函数值,以后不能更改,而变量定义通常引入一个带有初始值的标识符,但是以后可以更改。(这不是绝对的规则;大多数语言中都确实存在函数变量和非函数常量。)此外,在编译语言中,函数定义中绑定的值通常是编译时常量,因此可以对函数进行调用使用代码中的固定地址编译。在C / C ++中,这甚至是一个要求。相当于ALGOL 68

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

如果不引入函数指针,则不能用C ++编写。这种特定的限制证明对函数定义使用不同的语法是合理的。但是它们取决于语言的语义,并且这种证明并不适用于所有语言。


1
“数学家不喜欢˚F = ññ + 1在˚Fñ)= ñ + 1” ......更不用说物理学家,谁喜欢只写˚F = ñ + 1 ...
leftaroundabout

1
@leftaroundabout的原因是⟼很难写。老实说
皮埃尔·阿劳德

8

您以Java和Scala为例。但是,您忽略了一个重要事实:这些不是函数,这些是方法。方法和功能根本不同。函数是对象,方法是对象。

在具有功能和方法的Scala中,方法和功能之间存在以下差异:

  • 方法可以是通用的,函数不能
  • 方法可以没有一个,一个或多个参数列表,函数始终只有一个参数列表
  • 方法可以具有隐式参数列表,函数不能
  • 方法可以具有默认参数的可选参数,函数不能
  • 方法可以有重复的参数,函数不能
  • 方法可以具有按名称命名的参数,函数不能具有
  • 方法可以用命名参数调用,函数不能
  • 函数是对象,方法不是

因此,至少在这些情况下,您建议的替代品根本行不通。


2
“功能是对象,方法是对象。” 那应该适用于所有语言(例如C ++)吗?
svick 2014年

9
“方法可以具有按名称命名的参数,函数不能具有”-这不是方法和函数之间的根本区别,这只是Scala的一个怪癖。与其他大多数(全部?)相同。
user253751 2014年

1
从根本上说,方法只是一种特殊的功能。-1。请注意,Java(以及扩展为Scala)没有任何其他类型的功能。它的“函数”是具有一种方法的对象(通常,函数可能不是对象,甚至不是一流的值;它们是否取决于语言)。
2014年

@JanHudec:从语义上讲,Java同时具有功能和方法。在引用函数时,它使用术语“静态方法”,但是这种术语不会消除语义上的区别。
超级猫

3

我能想到的原因是:

  • 编译器更容易知道我们声明的内容。
  • 对我们而言(以微不足道的方式)知道这是函数还是变量,这一点很重要。函数通常是黑盒子,我们不在乎它们的内部实现。我不喜欢Scala中对返回类型的类型推断,因为我相信使用具有返回类型的函数会更容易:它通常是唯一提供的文档。
  • 最重要的是遵循以下用于设计编程语言的人群策略。C ++的创建是为了窃取C程序员,而Java的设计方式又不至于吓到C ++程序员,而C#却吸引了Java程序员。甚至C#(我认为这是一种非常现代的语言,背后都有一支很棒的团队)也从Java甚至C语言中复制了一些错误。

2
“……和C#吸引Java程序员” —这是有问题的,C#与C ++的相似之处远大于Java。有时似乎是故意使它与Java不兼容(没有通配符,没有内部类,没有返回类型协方差……)
Sarge Borsch 2014年

1
@SargeBorsch我不认为它是有意与Java不兼容的,我敢肯定C#团队只是试图正确地做到这一点,或者做得更好,但是您想看看它。微软已经编写了自己的Java&JVM并被起诉。因此,C#团队可能上进偏食的Java。它肯定是不兼容的,并且对此更好。Java刚开始时有一些基本的限制,例如,C#团队选择以不同的方式(在类型系统中可见,而不仅仅是糖)进行泛型,这让我感到高兴。
codenheim 2014年

2

回避这个问题,如果您对在内存受极大限制的计算机上编辑源代码不感兴趣,或者不希望将时间从软盘上读取的时间最小化,那么使用关键字有什么问题呢?

当然,它的更好的阅读x=y+zstore the value of y plus z into x,但这并不意味着标点符号比关键字本身“更好”。如果变量ijkIntegerxReal,考虑帕斯卡尔下面几行:

k := i div j;
x := i/j;

第一行将执行截断整数除法,而第二行将进行实数除法。可以很好地进行区分,因为Pascal div用作其截断整数除法运算符,而不是尝试使用已经具有另一目的(实数除法)的标点符号。

尽管在某些情况下可以简化功能定义(例如,作为另一个表达式的一部分使用的lambda)可能会有所帮助,但通常应该认为功能会脱颖而出,并且很容易在视觉上识别为功能。尽管可以使区分更加微妙,并且仅使用标点符号,但这样做的意义何在?说Function Foo(A,B: Integer; C: Real): String清楚了该函数的名称是什么,期望使用什么参数以及返回了什么。也许可以通过用Function一些标点符号代替它来将其缩短六到七个字符,但是会得到什么呢?

需要注意的另一件事是,在大多数框架中,始终将名称与特定方法或特定虚拟绑定相关联的声明与创建一个变量(该变量最初标识特定方法或绑定)之间存在根本区别。可以在运行时更改以标识另一个。由于在大多数过程框架中这些在语义上是非常不同的概念,因此它们应该具有不同的语法是有意义的。


3
我不认为这个问题与简洁有关。特别是因为例如void f() {}实际上比C ++(auto f = [](){};),C#(Action f = () => {};)和Java(Runnable f = () -> {};)中的lambda等效项短。lambda的简洁性来自于类型推断和省略return,但是我认为这与这个问题的要求无关。
svick 2014年

2

可以这么说,原因可能是这些语言的功能不足。换句话说,您很少定义函数。因此,使用额外的关键字是可以接受的。

在ML或米兰达(Miranda)遗产(OTOH)语言中,大多数时候都定义函数。例如,看一些Haskell代码。从字面上看,它基本上是一个函数定义的序列,其中许多具有局部函数和这些局部函数的局部函数。因此,乐趣在Haskell关键字将是错误的一样大,需要在命令式语言的assingment语句开始分配。原因分配可能是迄今为止最常见的陈述。


1

我个人认为您的想法没有致命的缺陷。您可能会发现它比使用新语法表达某些内容的预期要难,和/或您可能需要修改(添加各种特殊情况和其他功能等),但是我怀疑您会发现自己需要完全放弃这个想法。

您提出的语法看起来或多或少像某些表示法的变体,有时用于表示数学中的函数或函数类型。这意味着,像所有语法一样,它可能对某些程序员的吸引力要大于对其他程序员的吸引力。(作为一个数学家,我碰巧喜欢它。)

但是,你应该注意到,在大多数语言中,def风格的语法(即传统的语法)从标准的变量赋值不同的表现。

  • CC++家族中,函数通常不被视为“对象”,即要复制的大块键入数据,然后放在堆栈上,什么都没有。(是的,您可以具有函数指针,但通常仍指向可执行代码,而不是“数据”。)
  • 在大多数OO语言中,对方法(即成员函数)有特殊的处理;也就是说,它们不仅是在类定义范围内声明的函数。最重要的区别是,在其上调用该方法的对象通常作为隐式第一个参数传递给该方法。Python使用self(顺便说一句,它实际上不是关键字;您可以使任何有效的标识符成为方法的第一个参数)来使其明确。

您需要考虑新语法是否准确(并且希望直观)代表了编译器或解释器的实际操作。例如,它可能有助于了解lambda和Ruby中方法之间的区别;这将使您了解“功能为数据”范式与典型的OO /过程范式有何不同。


1

对于某些语言,功能不是值。用这种语言说

val f = << i : int  ... >> ;

是一个函数定义,而

val a = 1 ;

声明一个常量,会令人困惑,因为您使用一种语法来表示两件事。

其他语言(例如ML,Haskell和Scheme)将函数视为第一类值,但是为用户提供了一种特殊的语法来声明函数值常量。*它们正在应用“用法缩写形式”的规则。即,如果构造既常见又冗长,则应给用户简写。为用户提供两种完全相同的语法是不明智的。有时应该牺牲实用性。

如果用您的语言,函数是第一类的,那么为什么不尝试找到一种足够简洁的语法,使您不会被诱惑去寻找语法糖?

-编辑-

没有人提出(至今)的另一个问题是递归。如果允许的话

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

而你允许

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

是否遵循您应该允许的

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

在惰性语言(例如Haskell)中,这里没有问题。在基本上没有静态检查的语言(例如LISP)中,这里没有问题。但是在静态检查的急切语言中,如果要允许前两个允许并禁止最后两个,则必须谨慎定义静态检查的规则。

-编辑结束-

*有人可能会辩称,Haskell不属于此列表。它提供了两种方法来声明函数,但从某种意义上来说,这两种方法都是用于声明其他类型的常量的语法的概括。


0

这在类型不太重要的动态语言中可能很有用,但是在静态类型语言中可读性却不高,因为您总是想知道变量的类型。同样,在面向对象的语言中,了解变量的类型以了解其支持的操作也很重要。

在您的情况下,具有4个变量的函数为:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

当我查看函数头并看到(x,y,z,s)时,但我不知道这些变量的类型。如果我想知道z第三个参数的类型,则必须查看该函数的开头并开始计数1、2、3,然后再查看类型为double。在前一种方式中,我直接看了看double z


3
当然,有一个很棒的事情叫做类型推断,它使您可以使用var addOne = (int x, long y, double z, String s) => { x + 1 }自己选择的非moronic静态类型语言(例如:C#,C ++,Scala)编写类似的东西。甚至C#使用的其他非常有限的本地类型推断也完全可以满足此要求。因此,这个答案只是批评一种特定的语法,而该语法首先是可疑的,实际上并没有在任何地方使用(尽管Haskell的语法有一个非常相似的问题)。
阿蒙2014年

4
@amon他的回答可能是在批评语法,但同时也指出,问题中的目标语法可能并非对每个人都是“自然的”。

1
@amon对于每个人来说类型推断的用处不是一件好事。如果您经常阅读代码而不是编写代码,那么类型推断就不好了,因为在阅读代码时必须先推断出类型。并且比实际考虑使用哪种类型更快地读取类型。
m3th0dman 2014年

1
批评对我来说似乎是正确的。对于Java,OP似乎认为[输入,输出,名称,输入]是比简单的[输出,名称,输入]更简单/更清晰的函数def?如@ m3th0dman所述,具有更长的签名,OP的“清理”方法变得非常冗长。
2014年

0

在大多数语言中有这样一个区分的理由很简单:有必要区分评估声明。您的示例很好:为什么不喜欢变量?好了,立即对变量表达式求值。

Haskell有一个特殊的模型,其中在评估和声明之间没有区别,这就是为什么不需要特殊关键字的原因。


2
但是lambda函数的某些语法也可以解决此问题,那么为什么不使用它呢?
svick 2014年

0

在大多数语言中,函数的声明与文字,对象等的声明不同,这是因为函数的用法不同,调试方式不同以及构成错误的潜在原因。

如果将动态对象引用或可变对象传递给函数,则函数可以在对象运行时更改其值。如果将函数嵌套在复杂的表达式中,则这种副作用会使其难以执行该操作,这是C ++和Java等语言中的常见问题。

考虑在Java中调试某种内核模块,其中每个对象都有一个toString()操作。虽然可以预期toString()方法应该还原该对象,但是可能需要反汇编并重新组装该对象才能将其值转换为String对象。如果您尝试调试toString()将要调用的方法(在钩和模板方案中)以完成其工作,并且意外地在大多数IDE的变量窗口中突出显示了该对象,则可能会使调试器崩溃。这是因为IDE会尝试toString()对象,该对象调用您正在调试过程中的代码。原始值永远不会像这样废话,因为原始值的语义是由语言而不是程序员定义的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.