英文短语来源:为什么我们要“call”一个函数?

StackExchange上,有人提问为什么程序员要“call”一个函数。几个可能的隐喻浮现在脑海中:

  • Calling 函数就像拜访朋友——我们去那里,待一会儿,然后回来。
  • Calling 函数就像召唤仆人——召唤他们执行任务。
  • Calling 函数就像打电话——我们提出问题,并从外部获得答案。

真正的答案似乎是中间一个——“Calling”意为“召唤、呼唤”——但间接地,它源于从子程序库中“Calling”子程序的概念,就像我们从闭架书库中“Calling”一本书一样。元素周期表


《牛津英语词典》(OED)中“call number”(图书馆学意义上的“索书号(call-number)”)的首次引文来自梅尔维尔·杜威(没错,就是那位杜威),时间是1876年。《牛津英语词典》对其定义为:

图书馆书籍上标注的标记(尤其是数字),或在图书馆目录中列出的标记,用于指示书籍在图书馆中的位置;书籍的印刷标记或书架标记

我看到图书馆员在《图书馆期刊》13.9(1888年)中使用“索书号(call-number)”一词,仿佛该术语当时已广为接受:

大卫森先生读了一封来自A.W.泰勒先生的信[…],信中附上了普莱恩菲尔德(新泽西州)公共图书馆(P.L.)使用的全新索书单(call blank)样本,该样本为申请人的签名和地址留出了更多空间。[…] “关于泰勒先生的新借阅单(call slip)[…],我每次都要列出一长串借阅号(call numbers)来确认一本书,然后图书馆员保留这份清单,下次我又得重新做一遍,这让我感到非常气愤。”

根据《信息组织》 第4版(Joudrey & Taylor, 2017):

索书号(Call number)。资源上的一种标记,与元数据描述中的相同标记相匹配,用于识别和定位该资源;它通常由分类标记和卡特编号组成,还可能包括工作标记和/或日期。它是用于在闭架图书馆中“call”资源的编号;因此得名“索书号(Call number)”。

卡特编号(Cutter number)用于对具有完全相同分类标记的所有作品进行字母排序的标记。该术语以查尔斯·阿米·卡特命名,他设计了此类方案,但当指代非卡特本人设计的另一张此类表格时,应使用小写c


约翰·W·莫奇利(John W. Mauchly)的文章“为EDVAC型机器准备问题” (1947年)仅两次使用了英语单词“call”,但这似乎是该词在计算机子程序“库”语境下的重要早期例证:

机器用户面临的重要问题包括:如何轻松访问任何子程序?启动子程序的难度如何?终止子程序的条件是什么?以及控制权可以多轻松地返回原始序列的任何部分或进一步的序列[…] 用于条件和其他转移到子程序、转移到进一步的子程序以及返回的设施,肯定会被频繁使用。

[…] 参数在内存中的存储位置可以标准化,这样每次调用子程序进行计算时,子程序将自动知道要使用的参数位于指定位置。

[…] 其中一些可能被写入手册并根据需要转移到问题的编码中,但任何复杂的问题都应保存在一个中——即一组磁带,其中存储了具有永久价值的已编码问题。

[…] 在这种情况下必须解决的一个问题是,从库中提取子程序并按特定问题所需的正确顺序进行编译的方法。[…] 可能 […] 制定一套编码指令,将子程序放置在机器已知的内存位置,并以一种易于call的方式进行编码 […] 只需通过编号简要引用它们,正如它们在编码中所指示的那样。

《“MANIAC II 组装例程”手册》(1956年1月)基本上遵循了莫奇利的草图。MANIAC II拥有一个纸带“库”,其中包含可由汇编器调用的子程序,这些子程序可成为完整汇编程序的一部分。事实上,该“库”中的每个项目都拥有一个标识“call number”,就像真实图书馆中的每本书都有一个call number一样:

MANIAC II的汇编例程旨在将描述性代码转换为绝对代码。[…] 描述性磁带的大部分内容是一系列指令,通过控制字符分隔成编号组,称为“盒子”[因为流程图:今天我们会说“基本块”])。允许的盒子编号为01EF[。…] 如果指令的地址字段中的地址是FXXX,则该指令必须是转移指令,且转移目标是call number为XXX的子程序。最常见的子程序与汇编程序位于同一磁带上,并会自动加载。对于其他子程序,汇编程序会暂停,以便将相应的纸带放入光电阅读器。

注意,MANIAC II中的实际指令(或“命令”)仍被称为“TC”(转移控制),而程序的运行时行为被称为“控制转移”,而非“call”。此处的“调用”并非指运行时行为,而是指在汇编时调用编码的子程序,使其成为完全汇编程序的一部分。

Fortran II (1958年;另见此处)引入了CALLRETURN语句,其描述如下:

FORTRAN II 的新增功能使程序员能够无限扩展系统语言。[…] 每个 [CALL 语句] 将构成对定义子程序的 CALL,该子程序可执行任意长度或复杂度的过程 […]

[CALL] 语句将控制权转移到子程序 NAME,并向子程序传递括号中的任何参数。[…] 通过 SUBROUTINE 语句引入的子程序可通过指定子程序名称的 CALL 语句被主程序调用。例如,由

SUBROUTINE MATMPY (A, N, M, B, L, C)

可以通过以下语句被调用到主程序中:

CALL MATMPY (X, 5, 10, Y, 7, Z).

请注意,Fortran II 仍将运行时行为描述为“控制转移”,但随着计算机语言向更高层次发展,英语开始模糊并混淆运行时控制转移行为与汇编时或链接时“call”行为。

在罗伯特·I·萨巴赫的《电子与核工程百科全书词典》 (1959年)中,子程序条目并未使用“call”一词,但萨巴赫似乎在某种程度上反映了莫奇利定义与Fortran II定义的结合体中的思维模型。

Call in在计算机编程中,指将计算机控制权从主程序转移到插入到计算操作序列中的子程序,以执行辅助操作。

Call number在计算机编程中,用于标识子程序的一组字符。它们可能包含与操作数相关的信息,也可能用于生成子程序。

Call word 在计算机编程中,长度正好为一个字word的Call number。

注意,萨巴赫将“call”定义为运行时控制权转移本身;这与Fortran II手册中对该术语的使用方式不同。可能萨巴赫准确地反映了1958年至1959年间口语含义的实际变化——但我个人认为他可能只是搞错了。(萨巴赫是一名受过高度训练的物理学家,但据我所知,他并非计算机专家。)

“JOVIAL:语言描述”(1960年2月)中写道:

一个过程 call[今天我们称之为“call site”]是主程序与过程之间的链接。它是过程唯一可被调用的位置。

一个输入参数 [今天我们称之为“argument”] 是过程调用中指定的算术表达式,代表过程要操作的值[.] 一个虚拟输入参数 是过程声明中指定的项,代表过程作为输入参数使用的值。

一个或多个 过程call(其他过程的call)可能出现在一个过程中。目前,仅存在四种“级别”的 call

该JOVIAL手册不仅提到了“过程call”(用于将控制权转移到过程声明的语法),还提到了“SWITCH调用”(用于将控制权转移到开关-案例标签的语法)。也就是说,JOVIAL(1960年版)已完全采用名词“call”来表示“运行时控制转移的语法指示符”。然而,JOVIAL从未将“to call”用作动词。

回溯几个月,这是Perlis与Samelson的“初步报告——国际代数语言”(《计算机协会通讯》2(6),1959年6月):

一个过程语句用于call一个过程的执行,该过程是一个封闭且自包含的流程[…] 定义被调用过程的程序声明在其标题中包含与过程语句形式相同的符号串,而形式参数[…]则提供了关于任何过程调用中使用的参数合法性的完整信息[.]

彼得·诺尔的《Algol 60报告》 (1960年5月)避免使用动词“call”,但在新发展中随意使用名词“call”来表示“过程本身正在运行的时期”——不是控制权的转移,而是转移之间的时间段:

过程语句用于call过程主体的执行。[…] [当传递数组参数时,如果]形式参数按值调用,则在调用期间创建的局部数组将与实际数组具有相同的下标范围。

最后,“Burroughs代数编译器:用于Burroughs 220数据处理系统的ALGOL表示法” (1961)中,动词“call”的无介词形式出现了一个(定义性)实例:

ENTER 语句用于启动子程序的执行(即“call”子程序)。

《高级计算机编程:课堂汇编程序的案例研究》(Corbató、Poduska 和 Saltzer,1963)中的用法完全符合现代用法: “对于传递一而言,调用子程序来存储卡片仍然很方便”;“为了callEVAL,必须保存临时结果”;“callPASS1PASS2的子程序”;等等。

因此我目前的推测是:

  • Fortran II(1958年)迅速普及了“调用X”这一表述,用于临时转移控制权至X,因为在Fortran II程序中,当你希望将控制权转移至名为X的程序时,你需要直接编写CALL X
  • Fortran 自身选择的“CALL”助记符是一个原创新词,灵感来源于 Mauchly 和 MANIAC II 引文中已存在的“call (in/up)”用法,但引入了在 Fortran 之前从未见过的独特变化。
  • 到 1959 年,Algol 从 Fortran 借用了“call”。Algol 的“过程语句”在运行时产生call;一个过程可以被call;在call过程中,该过程将执行其工作。
  • 到 1961 年,我们首次看到“call X”这一确切短语的使用。

共有 168 条讨论

  1. 这种“调用”的原始含义(源自物理图书馆中用于整理书籍和其他材料的“索书号”)也促成了“编译器”这一术语的诞生,据格蕾丝·霍珀所述: “之所以在1952年左右被称为‘编译器’,是因为每个子程序都被赋予了一个‘调用词’,因为子程序位于一个库中,而当你从库中提取东西时,你就是在编译东西。就是这么简单。”

      1. 可惜我们不能使用cast,因为它已经被用于类型。而conjure可能只适用于对象构造器。

        1. Erlang/Elixir在向其GenServer演员进程发送消息时使用“cast”方法名称。

          1. 有两个术语。

            * call – 发送消息并等待回复 * cast – 发送消息而不等待回复

        2. 而 ‘summon’ 仅用于守护进程。

          1. 专用于网络调用的关键字,特别是微服务 ahaahaha

      2. “invoke”的同义词包括“call forth”和“conjure up”。

        1. 或“拍摄清单”,即特定电影拍摄所需的演员和工作人员名单

      3. 功能派的人甚至会“应用”它们。

      4. 同上,但我会说“函数调用”,而不是“函数调用”。

        调用X听起来颇具炼金术的韵味,顺便说一句。

    1. 我刚联想到……杜威十进制分类法中的标识数字被称为“索书号”!

      1. 是的,这在文章的第二段中提到过。

  2. 我认为图书馆学对现代计算的贡献远超我们的想象。

    例如,在解释数据库索引时,我常会提到卡片目录的图像。当人们看到索引卡片,然后看到有木制盒子用于按作者检索、单独的盒子用于按杜威十进制分类检索等,他们就会豁然开朗。

    https://en.wikipedia.org/wiki/Library_catalog

    1. 我足够年长,曾在当地图书馆使用过(纸质)词典和木制盒子卡片目录。因此,当我二十五年前第一次听说哈希表/IDictionary时,正是这个形象帮助我理解了这个概念。

      然而,这个比喻现在已经不再那么具有教育意义了。我曾多次解释卡片目录甚至(书籍)词典的工作原理,却只得到这样的回应:“哦,所以它们基本上就是模拟哈希表。”

    2. 年轻人可能从未见过卡片目录。

      我只是解释说硬盘只是由1和0组成的连续列表,然后问如果人们想找到任何东西,我们需要做什么。人们能够推断出需要某种结构的必要性。

    3. 我一直认为书籍末尾的索引是计算机术语“索引”的起源。索引卡与索引的关系我从未想过!

      1. 索引卡与书籍中的索引条目并无不同。索引在拉丁语中意为“指示器”或“指针”(因此手指得名)。

        1. 是的,我了解这个词汇,只是从未将索引卡与这种用途联系起来,因为我们小时候只用索引卡来标记、记笔记、做手工和制作闪卡。

    4. 几个月前,我一直在想,为什么终端的“标准”宽度是80个字符?我以为这与早期PC的屏幕尺寸有关。

      但事实并非如此,是因为穿孔卡的宽度是80个字符。而最早的穿孔卡基本上就是索引卡。再次向图书馆员致敬。

      我想这相当于计算领域的“汽车宽度等于两匹马的臀部”……

      1. 计算领域中使用穿孔卡(可争议地)受到了纺织业的启发。打孔卡早在18世纪就被用于配置织机。

    5. 绝对没错!我承认我以为这是教学中明确包含的内容。索引中的“投影”属性就是你可以在卡片上放置的内容。我惊讶于这么多人似乎对这些东西的工作原理没有概念。

    6. 即使它确实贡献更多,它对现代计算的贡献也微乎其微。

  3. 我是芬兰人,在芬兰语中,函数上下文中的“调用”翻译为“kutsua”,翻译回英语时成为“邀请”或“召唤”。

    因此,至少在芬兰语中,“call”一词被认为在“母亲把孩子们从院子里叫回屋里”这样的语境中使用,而不是像“乔给朋友打了个电话”或“你管这种颜色叫什么”这样的用法。

    只是想分享一下。

    1. 在德语中,我们使用“aufrufen”,如果逐字翻译,意思是“召唤”,而在计算机时代之前(据我所知),当与直接宾语一起使用时(如在功能中),它仅被理解为“通过名字或号码召唤某人”(例如老师让学生发言或站起来)。

      它也与“打电话”的动词“anrufen”不同。

      1. 有趣!在瑞典的湖对岸,我们确实使用“anropa”来调用子程序。我从未在这种情况下听到有人使用“uppropa”,而“uppropa”是“aufrufen”的直接翻译。

    2. “召唤”一词在代码中带有一丝诡异恐怖的意味,这在某些情况下非常合适。“邀请”也可能暗示像恶魔或吸血鬼一样的存在,这也说得通!

      1. 一个有趣的补充和/或后续内容:

        > 《计算机程序的结构与解释》(SICP)是麻省理工学院教授哈罗德·阿贝尔森和杰拉尔德·杰伊·苏斯曼与朱莉·苏斯曼合著的计算机科学教材。它在黑客文化中被称为“巫师之书”。[1] 该书教授计算机编程的基本原理,包括递归、抽象、模块化和编程语言的设计与实现。

        https://en.wikipedia.org/wiki/Structure_and_Interpretation_o…

        另一个例子:

        > “存在,它便存在”(阿拉伯语:كن فَيَكُونُ;kun fa-yakūn)是《古兰经》中的一句经文,指的是上帝以命令创造万物。[1][2] 在阿拉伯语中,这句话由两个词组成;第一个词是kun,意为命令式动词“存在”,由字母kāf和nūn组成。第二个词 fa-yakun 意为“它就成了[完成]”。[3]

        > (第2章117节的图片)https://commons.wikimedia.org/wiki/File:002117_Al-Baqrah_Urd…

        > 2:117节末尾的短语“Kun fa-yakūn”在《古兰经》中被引用为真主至高无上创造力的象征或标志。《古兰经》中共有八处提及该短语:[1]

        https://en.wikipedia.org/wiki/Be,_and_it_is

        我好奇“Be”是命令式还是功能性用法。“Be”是`Unit()`的另一个名称吗?还是更像Lisp风格的`(be unit)`?

    3. 在挪威,它被称为“funksjonskall”,即字面意思上的“函数调用”。而“kall”/“call”就是指对某物的调用。

    4. 在俄语中,情况类似,直译为“打电话”、“召唤”、“邀请”。

  4. > … 为什么我们要“调用”函数?

    我一直认为函数不需要调用关键字,因为它们通常会返回一个值,因此函数会出现在赋值语句中。所以只需使用函数即可。

    需要调用的是子程序,它本质上是一个命名地址/标签。

    确实,也可以直接跳转到地址/标签,然后再跳转回来。CALL 关键字使整个操作更加全面。

    因此,从某种意义上说,这类似于使用地址号码拨打电话。通常,这会改变一些共享状态,以便调用者在调用后继续执行。可以将其想象成老板先叫 Sam 计算数据,然后叫 Bill 漂亮地打印 TPS 报告。

    最终一切都变成了函数,子程序则与“意大利面代码”联系在一起……

    那么,为什么它被称为“例程”(即程序)和“子程序”?

  5. 偶尔也会听到“调用”或“执行”等更冗长但更通用的表述。

    顺便说一句,我认为“调用”(如“调用命令”、“调用按钮”)是ESL计算机科学学生使用的一些令人不快的短语之一。

    1. “调用”源自拉丁语invocō、invocāre,意为“呼唤”。我不会认为这是误用,而是简化。

      1. > “Invoke” 源自拉丁语 invocō, invocāre,意为“呼唤”。

        (就像你呼唤一项技能一样,而不是像呼唤邻居那样。)

        1. 但 vocare(invoco 中的 voco)是用来呼唤邻居的。

        2. 这是通过大声喊出某人的名字来呼唤一个人。

        3. 这与调用函数非常契合——你使用它的技能,而不是呼唤它聊天。

    2. > 对“呼唤”的奇怪误用

      我最喜欢(或者说最不喜欢?)的是将“call”与“return”结合使用。我曾不止一次听到有人说:

      “当我们调用return关键字时,函数就会结束。”

      1. 我记得大学里有人提到过if函数(该函数表面上只接受一个布尔参数)。

        1. 在Excel公式中,一切都是函数。IF、AND、OR、NOT都是函数。这虽然有些别扭,与软件开发人员习惯的模式相悖,但熟悉Excel IF函数的人可能比熟悉其他形式的人更多。以下是一个来自文档的示例…=IF(AND(A3>B2,A3<C2),TRUE,FALSE)

          1. Excel单元格公式是全球使用最广泛的函数式编程语言。

        2. 这听起来像是约翰·奥斯特豪特教授会说的话:-; 这种说法在 Tcl 中是字面意义上的准确。

          我对 Smalltalk 的了解不够,无法确定,但我记得它也有类似的“一切都是对象”的理念,我不会惊讶于他们以某种方式将控制流强行纳入这个框架。

          Forth 也让我联想到,但这可能有点牵强。

          1. > 我对 Smalltalk 的了解不够深入,无法确定,但我记得它也有类似的“一切皆对象”的理念,如果他们将控制流以某种方式融入这个框架,我也不会感到惊讶。

            确实如此。这在HN上之前就讨论过:https://news.ycombinator.com/item?id=13857174

          2. 我会包含Lisp中的cond函数,或者lambda演算的推广

               λexpr1.λexpr2.λc.((c expr1) expr2)
            
        3. 确实有一些语言中,`if` 是一个函数。

          在 Tcl 中,`if` 被称为“命令”。

          1. 在Smalltalk和sclang(Supercollider语言)中也是如此

        4. 我经常看到人们将if视为“进行比较”,因此:if (variable == true) …

        5. if 接受两个或三个参数,但绝不会接受一个参数。条件是大多数语言中语法上明确指定的部分,后置语句是另一个必需的参数,而替代语句是可选的。

          1. 嗯?if (true) {} 恰好接受一个参数。

            1. 这取决于语言!如果“if”不是关键字,在 Ruby 中这将调用一个接受一个位置参数和一个块参数的方法,例如 `def if(cond, &body) = cond && body.call`。在 PureScript 中,这可能是对一个具有签名 `if :: Boolean -> Record () -> _` 的函数的调用。

              但我假设你回复的评论并非指代 C 类语言中的条件语法,而是指代一种“if 函数”的概念,如 Julia 中的 `ifelse` 函数 [1] 或 Lisp 中的 `if` 形式(其语法与函数/宏调用相同,但实际上是一种特殊形式)[2], 这两者均不适合作为单参数函数。

              [1] https://docs.julialang.org/en/v1/base/base/#Base.ifelse

              [2] https://www.gnu.org/software/emacs/manual/html_node/elisp/Co…

            2. 这是对 `if` 的应用,其中一个参数为空。

              `if` 的语义要求至少包含 `if(cond, clause)`,尽管更一般地,可以是 `if(cond, clause, else-clause)`

              1. 你和 Zambyte 都在做顶层注释所抱怨的事情。

                例如在 C 中:

                https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf

                    (6.8.5.1) 选择语句:
                      if ( 表达式 ) 次要代码块
                      if ( 表达式 ) 次要代码块 else 次要代码块
                

                在 C++ 中:

                https://eel.is/c++draft/gram.stmt

                    选择语句:
                      if constexpropt ( 初始化语句可选 条件 ) 语句
                      if constexpropt ( 初始化语句可选 条件 ) 语句 else 语句
                      if !opt consteval 复合语句
                      if !opt consteval 复合语句 else 语句
                

                其中

                    条件:
                      表达式
                      属性指定符序列(可选)声明指定符序列声明符大括号或等号初始化器
                      结构化绑定声明初始化器
                

                更多示例:

                https://docs.python.org/3/reference/grammar.html

                https://doc.rust-lang.org/reference/expressions/if-expr.html…

                https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe…

                表达式不等于参数

                1. 他们并不是在特指C语言及其衍生语言,而是更广泛地讨论。例如在Haskell和Scheme中,只有if函数而没有if语句。你可以用任何语言创建一个if函数并用它替代原生语法。我喜欢在PostgreSQL中使用if函数,因为它比case表达式更简洁。

                  因此,从抽象意义上讲,if是一个三元函数。我认为原评论是在反思“if (true) …”看起来像是一个带有一个参数的函数调用,但显然这是错误的。

                  1. 这并不完全正确。Haskell和Scheme有if表达式,而不是if语句。这与 if 是函数不同。在 Scheme 中,if 不是,也不能是函数,因为它不具备 Scheme 函数的语义。具体来说,它不是严格的,因为它在执行前不会评估所有子表达式。由于 Haskell 是非严格的,if 可以实现为函数,据我所知确实如此

                    1. > 由于 Haskell 是非严格的,if 可以实现为函数,而且我记得它是

                      “If” 可以实现为 Haskell 中的函数,但它不是函数。你不能将其作为高阶函数传递,而且它还使用了 ‘then’ 和 “else” 关键字。但如果你想的话,可以将其实现为函数:

                        if' :: Bool -> a -> a
                        if' True x _ = x
                        if' False _ y = y
                      

                      然后,你不需要像这样写:

                        max x y = if x > y then x else y
                      

                      你会这样写:

                        max x y = if' (x > y) x y
                      

                      但“then”和“else”消除了对表达式周围括号的需求。

                  2. 在 Haskell 中,参数是表达式。从抽象意义上说,它使用表达式。

        6. 尝试在其他语言中实现这一点,你会遇到问题。

          在采用立即求值的命令式编程语言中,即参数在函数应用前就被求值,将if实现为函数会同时求值“then”和“else”分支,若分支存在副作用,这将导致不可预期的行为。

          在纯粹但仍采用热评估的函数式语言中,如果分支无法产生副作用,这种实现方式可能更有效。但它仍然低效,因为你正在评估结果会被丢弃的表达式,这只是浪费计算资源。

          在惰性函数式语言中,你可以拥有一个可行的 if 函数,因为它只会评估所需的参数。但即使在惰性函数式语言 Haskell 中,if 也是作为内置语法实现的,出于易用性考虑——如果编译器理解 if 的含义而非将其视为普通函数,它可以进行更好的优化、生成更好的错误信息等。

          在具备合适宏的语言中,你可以将`if`定义为宏。通常在这种情况下,宏会将参数包裹在lambda表达式中,以便仅在需要时进行评估。但 Scheme 和 Lisp 虽然具备合适的宏,却未将`if`定义为宏,原因与 Haskell 类似。

          在纯lambda演算中,if是一个函数,但没有人用它来编写实际代码。

          我能想到的唯一一个将if定义为函数(实际上是几个方法)的“主要”语言是Smalltalk,而在这种情况下,它能正常工作是因为其参数是代码块,即本质上是lambda表达式。

          tl;dr:在大多數語言中,將 if 作為函數使用並不實用。

          1. 在 Smalltalk 中也不實用,因此編譯器會做一些特殊處理:

                ifFalse: alternativeBlock
                    "返回 alternativeBlock 的值。實際上執行並不會到達這裡,
                    因為表達式會被內聯編譯。"
            
                    ^替代块值
            
            1. 哦,谢谢,我不知道这一点。我以为它只是依赖于显式的代码块。

              但确实,这是优化中的一个关键点——任何现实的语言迟早都会优化这一点。

          2. 我认为 Haskell 没有必要将 ‘if’ 作为编译器优化的构造;它可以通过模式匹配轻松实现:

            if’ :: Bool -> a -> a -> a

            if’ True x _ = x

            if’ False _ y = y

            如果编译器知道第一个参数是常量,它可以替换这个表达式。

            也许在早期版本中需要它。或者也许他们只是不知道他们不需要它。Haskell 的早期版本也有相当糟糕的 I/O。

            1. 对于 `if` 的函数版本,编译器通常需要将替代方案包装在闭包(“thunks”)中,就像它对所有函数参数所做的那样,除非优化使它不再必要。在语法版本中,这从未被需要。这是其中一个重要的优化。

              在GHC中,`if`会被编译为一个case语句,许多优化都由此衍生。它在编译器的运行中占据了相当重要的地位。

              > 也许在早期版本中需要它。或者也许他们只是不知道自己还不需要它。

              这两种情况都不成立。我上面的评论试图解释为什么 `if` 没有被实现为一个函数。Haskell 就是一个很好的例子,说明它本可以这样实现,作者们完全清楚这一点,但他们没有这样做,因为反对这样做的理由非常充分。(除非你在实现一种脚本语言类型的系统,其中你不在乎优化。)

            2. 简短的搜索导致了这个 SE 帖子 [1],它没有回答“为什么”,但说“if”只是语法糖,转换为 `ifThenElse`…

              [1]: https://softwareengineering.stackexchange.com/questions/1957…

              该帖子声称这种实现方式非常基础,如果你成功重绑了`ifThenElse`,你的重绑函数会被调用。我没有确认这一点,但相信它是正确的。

      2. 有些人使用括号来表示返回值,使其看起来像函数调用:

            return(value);
        
      3. 嗯,“return”只是一个带有特殊语法的非常受限的延续……说你“调用”它有点牵强,但并非毫无依据。

      4. 我也听说过——我脑海中的声音自动用惯常的浓重印度口音读出这句话。

    3. >顺便说一句,我发现ESL计算机科学学生使用“调用”的奇怪误用(如“调用命令”、“调用按钮”)是令人不快的短语之一。

      根据我自己的经验,母语为英语的初学者(在编程方面)也会这样做。他们也会将各种不是命令的东西描述为“命令”。

    4. C#似乎喜欢用“Invoke”来处理委托或反射方法。然后它在调试器视图中使用“调用堆栈”。

      1. 微软开发人员按字符计酬,我不确定这是否算数。

    5. 我实际上经常看到新手将语句(甚至整个函数声明)称为“命令”。

      1. “命令”是描述我们所称的“语句”在命令式编程语言中的更好术语。“语句”在这个上下文中是一个不幸的历史术语;除了Prolog之外,这些“语句”没有真值,只有效果。(而在Prolog中,我们称它们为“子句”。)

        1. 对真值的要求仅仅源于数学/逻辑对该术语的使用方式。

        2. 是这样吗?“语句”一词,根据词典定义为“通过除文字以外的其他方式表达思想或观点”,似乎非常贴切。符号可能最终会类似于文字,这或许是你的观点,但从技术上讲,它们只是符号。

          据我所知,所有可用的“命令”定义似乎都暗示了相关的动作,而这并非所有命令式编程中的陈述都具备。

        3. 确实。

          在许多早期计算机编程文档中,术语“命令”被用于替代“陈述”,其中“命令”意为“指令”的同义词,而非指代序列的排列顺序。

          1. 偶尔会这样,但更多情况下(如Mauchly引用的论文中所示),“命令”指的是机器指令,而非高级语言中的“语句”。

            1. 是的,但这主要是因为在最初的几年(包括莫奇利时代),还没有“高级”编程语言,因此构成程序文本的“命令”直接对应于机器可执行的指令。

              我认为“语句”这一术语是由IBM关于FORTRAN的出版物在1956年开始使用的。

              在IBM FORTRAN的首次公开文档之前,1954年的首份内部FORTRAN文档使用“公式”一词来指代后来被称为“可执行语句”的任何内容,即许多在当时或之后都不会被称为公式的事物,如IF公式、DO公式、 GOTO公式等,而该文档则使用“句子”来指代后来被称为“非可执行语句”(即定义或声明)的内容。

              在FORTRAN(1951年至1953年)之前,海因茨·鲁蒂豪瑟(Heinz Rutishauser)在其高级编程语言中使用了“Befehl”一词,意为“命令”。(对于我们今天所称的“程序”,他使用了“Rechenplan”一词,意为“计算计划”。)

    6. 在老款诺基亚手机上,点击链接需按下通话按钮。

    7. 如今普遍使用“轻触或点击按钮”,我或许能接受下一代使用“通话”一词。只要别用“兄弟,按钮”就行。

  6. [Wilkes, Wheeler, Gill](https://archive.org/details/programsforelect00wilk) [1951] 使用“调用”一词来调用子程序。

    第31页有:

    > … 如果由于程序员的错误,命令 Z F 没有被覆盖,机器将立即停止。这种情况可能发生在子程序未正确调用时。

    > 需要注意的是,一个闭合的子程序可以从程序的任何部分调用,且没有限制。特别是,一个子程序可以调用另一个子程序。

    参见第33页的程序。

    互联网档案馆收录了1957年版的书籍,因此我不确定与1951年版相比,这段表述是否有所更改。我找不到一篇关于1950年左右的EDSAC的论文,可以轻松阅读,但[这里有一份关于EDSAC早期年份的文物图片的演示文稿](https://chiphack.org/talks/edsac-part-2.pdf)。其中包含1950年《关于为EDSAC准备程序及使用子程序库的报告》中的几页内容,展示了一个子程序列表,其中注释写道“调用辅助子程序”。

  7. Algol 60 同样使用“调用”一词来表示参数和函数。它引入了(?)“按值调用”和“按名调用”这两个术语。例如,在 4.7.5.3 中提到:“此外,如果形式参数是按值调用,则在调用过程中创建的局部数组将具有与实际数组相同的下标范围。”

    在现代术语中,我们“调用”过程/函数/子程序,并“传递”参数/参数,因此“按值/名/引用传递”比“按值/名/引用调用”更清晰。但“按值调用”等旧术语在某些上下文中仍被保留,尽管“调用”参数或参数的概念已不再使用。

  8. > … 但任何复杂的程序都应保存在库中——即一组磁带,其中存储了具有永久价值的已编码问题。

    奇怪的是,我从未将“库”一词与物理上标记和组织好的磁带架联系起来,直到现在。

    1. 我从未听说过库文件有其他名称——例如,常见的文件扩展名.lib就是一个例子。

      1. 我认为.lib并不是特别常见,但这可能只是我习惯了而已。`.so`或`.dll`等扩展名当然也存在(尽管公平地说,后者确实包含“库”这个词)。

        1. .lib 是 Windows 上静态库和导入库的传统扩展名。每个 .dll 都有对应的 .lib。(Msys2 使用自己的扩展名,即 .a 用于静态库,.dll.a 用于导入库。)

      2. 问题不在于它们被称为“库”,而在于为什么被称为“库”。我曾像许多人一样认为,这纯粹是类比(即桌面),而非该术语源自物理磁带库。

  9. 音乐中也有“呼应”这一术语——甚至涉及返回值。

  10. 这不是科学理论,而是观察。新词汇传播时会“产生共鸣”。它们通常简短,因某种原因能帮助人们建立心理联想并记住其含义。它们像病毒一样迅速在人群中传播。有时需要解释,有时人们从上下文中理解,但之后人们倾向于记住并使用这些词汇,进一步传播该词。

    一个较新的例子是“salty”。它很短,而且感觉上似乎能描述其含义(salty -> 眼泪 -> 难过)。

    “call”似乎也有类似情况。它很短,作为一个常用的技术术语,发音简单,而且有几种方式能让人觉得“恰到好处”:召唤、呼叫、召回、施法(作为魔法咒语)。人们听到它,觉得合适,于是这个术语就传播开来。我怀疑当时不会有太多竞争术语,因为像“jump”这样的术语已经用于指代现有概念。此外,当时电话是热门的魔法般技术,大约在同一时期普及开来。能够“呼叫”某人的想法会占据人们的脑海,因此当代程序员很可能轻易地在“呼叫他人”与“呼叫子程序”之间建立起心理联系/类比。

    1. 附注:对我来说,“salty”与眼泪无关;在我个人的语言习惯中,当有人“salty”时,并不意味着他们“悲伤”,而是意味着他们“愤怒”或“受辱”之类的情绪。这个比喻更多是关于盐(在大剂量下)味道浓烈而尖锐。

      (这或许说明,即使大家对比喻所指的具体事物没有共识,比喻仍可能成功,正如你所提到的“调用”可能做到的那样。)

      1. “Salty” 在那个语境中意味着“苦涩”,这颇具讽刺意味。

        1. 我会用“怨恨的”、“不满的”、“受委屈的”来形容。对我来说,“苦涩”更像是某种持续时间更长、情感强度稍弱的状态。

          我同意这与眼泪无关。该词的实际词源可追溯至水手:https://www.planoly.com/glossary/salty

          1. 确实,语言(尤其是英语)中存在无数微妙之处,使词汇既模糊又极具特定性。

            维基词典将“bitter”定义为“愤世嫉俗和怨恨的”,这并不能完全捕捉到“更持久、情感上稍弱”的部分。

  11. 有点离题,但视频通话中的动词“jump”是从哪里来的?我总是被要求“jump on a call”

    1. 我猜是因为你在做一件你宁愿不做但为了别人利益的事,就像你会跳到一颗手榴弹上一样 :p

      1. 我将从此把这个作为标准解释 🙂

    2. 我以为这就像跳上公交车。然后“跳”就很容易变成“跳上”。

    3. 在视频通话出现之前,你就会跳上通话。甚至不一定是会议通话,你也可以跳上电话等。

      这只是意味着开始做某事,没什么神秘的。

    4. 我一直以为这只是一个比喻,指突然离开你正在做的事情。你在做其他事情,然后你“跳”进一个电话。你不会慢慢走过去,十五分钟后才出现在电话里。

    5. 它似乎带有即兴和不需要准备的含义。

    6. 这只是“跳”这个词的标准含义。你从正在做的事情跳到视频通话。

    7. 这是Zoom收购乐队House of Pain的结果。

    8. 也许这是暗示操作不可逆转! 🙂

    9. 这与视频功能无关,而是与群组功能相关。“跳转”、“跳跃”等术语将群组通话类比为公交车行程,人们可以随时上下车。

  12. 我一直觉得奇怪的是,呼叫方和被呼叫方需要达成一致的唯一事项被称为参数。

  13. “呼叫”的另一个用法是在谷仓/民间/乡村舞蹈中,呼叫者会喊出舞步指令。“摇摆你的搭档!”“Dosey do!”“上下中间!”等等。每个指令描述了一个不同的算法步骤。然而,这种词源与函数调用无关:每个呼叫都会修改舞者的全局状态,而不会向呼叫者返回任何值。

  14. 另一个有趣的用法是在游戏中,当一个效果被说成“触发”时,我猜这是来自一个过程被调用。

    1. 基本上,是的。它实际上起源于MUDs,来自“spec_proc”,即“特殊过程”的缩写。

    2. 我的朋友们似乎都认为这个词源自“采购”(procure)。

  15. 如果图书管理员所说的“调用”含义确实是最初的本意,那么即使在莫奇利1947年的文章中,你已经可以看到向更面向对象或面向角色的“调用”含义的转变。

    1. 这是一个很大的“如果”,因为支持这一理论的证据似乎比“调用”含义出现晚了十年。

      1. 说实话,我不确定你在这里说什么,或者你认为我在说什么。

        1. 你写道:“如果图书馆员的‘呼叫’含义确实是最初的意图”

          我怀疑“图书馆员的‘呼叫’含义确实是最初的意图”(并非你这么说!)。

          或者也许你所说的“呼叫”与第一个引文中的“呼叫”没有区别。子程序被调用/检索——控制权转移到该子程序。(我不会说当医生被叫去查看病人时,他们是被以图书馆员的含义‘召唤’的。)

  16. 这很容易。难的是我们该如何称呼函数。

  17. 将“调用”视为“召唤”函数是很有趣的。我们也可以合理地使用“实例化”、“评估”、“计算”、“运行”、“执行”(如COBOL中所用)或简单地“执行”。

    在莫奇利的《为EDVAC型机器准备问题》一文中,部分内容被博客文章引用,他写道:

    > 需要提供指令的操作总数通常会极其庞大,因此指令序列将远超内部存储器容量。然而,这样的指令序列绝非随机序列,通常可由频繁重复的子序列合成.

    > 通过提供必要的子序列(可按需重复使用)以及一个主序列来指导这些子序列的使用,可以实现针对非常复杂问题的高效且易于设置的指令。

    他在这里用于子程序调用的动词是“利用”和“指导”。在论文的后半部分,他使用“子程序”一词而非“子序列”,并且确实使用了“被调用”这一表述,但并非指机器中的子程序调用操作:

    > _对于这些,可以准备一次包含操作所需命令序列的磁带,并在特定问题中需要时提供使用。为了使这些子程序(它们可以被称为子程序)真正通用,机器必须具备修改指令的能力,例如将特定数量放入通用子程序中。由此形成了一组新的操作,可以说是指令的微积分。

    当然,如今我们不再通过修改子程序的代码来传递参数,但索引寄存器尚未发明,因此每个被引用的内存地址都必须包含在引用它的指令中。(这被认为是将程序保存在数据内存中的一个巨大优势!)

    稍后他提到“调用子程序”和“将控制权转移到子程序”,并讨论了从“库”中链接子程序,正如帖子中所引用的。

    他从未将子程序称为“函数”;我不确定这种用法从何而来,但到了BASIC和LISP时代,确实存在由子程序实现的“函数”。他确实提到了由子程序计算的“数学”函数,包括矩阵乘法等:

    > 如果子程序只是为了计算单个参数的函数,(…)

    1. “他从未将子程序称为‘函数’;我不确定这种用法从何而来,但到了BASIC和LISP时代,‘函数’至少是通过子程序实现的。”

      我认为早期BASIC使用子程序命名法来表示GOSUB,其中没有参数传递或其他功能,只是一个自动记住返回位置的跳转。

      BASIC中的函数,据我所记得,是完全不同的东西。我认为它们只是算术表达式的命名缩写,仅限于简单的单行算术表达式。它们更类似于非常原始且高度受限的宏,而非子程序或函数。

      1. 没错,Algol-58的函数也是如此。我认为FORTRAN也有类似的构造,但记不清了。

        1. FORTRAN同时具备函数和子程序。函数返回一个值,并在表达式中调用(例如:S=SIN(A))。子程序通过调用它来执行(例如:CALL FOPEN(FNAME, PERMS))。

          1. 我可能应该直接谷歌一下,但你是如何定义这些函数的?

            1.     C     -------- 函数开始 -------    
                        INTEGER FUNCTION INCREMENT(I)
                        INCREMENT=I+1
                        RETURN
                        END
                  C     -------- 函数结束 -------
              
              1. FORTRAN也有单表达式函数定义,例如

                    ARGF(X, Y, Z) = (D/E) * Z+ X** F+ Y/G
                

                当然,这在语法上与数组元素赋值完全相同,而这正是编译FORTRAN如此“有趣”的原因之一。

                1. 是的,这几乎与 Algol-58 定义此类函数的语法完全相同。BASIC 也是如此,只是你必须这样写:

                      DEF FNF(X, Y, Z) = (D/E) * Z + X**F + Y/G
                  

                  而且函数名必须以 FN 开头。

                2. s/had/has/

                  在全新的编译器中,该编译器会在处理任何声明之前先为整个源文件构建解析树,因此必须首先解析诸如语句函数之类的内容,以便后续的规范语句能够跟随其后。后来,如果发现函数名是一个数组或返回指针的常规函数,解析树会在原地进行修补,而该语句将成为第一个可执行语句。

  18. 我喜欢这种计算机科学历史。我也好奇——为什么我们要“抛出”错误或“引发”异常?为什么循环使用“for”而不是“loop”?

    1. 我猜“throw”这个词是在有人决定“catch”错误之后出现的。

      至于“raise”,也许异常应该被称为“objection”。

      1.   RuntimeObjection: 尸检报告在交叉询问期间更改了内容
        
    2. 已经很久了,但我认为斯特劳斯特鲁普(Stroustrup)的《C++编程语言》早期版本中解释过,他特意选择“throw”和“catch”是因为更直观的选择如“signal”已被现有的C程序占用,而选择“不寻常”的词汇(如“throw”和“catch”)能降低冲突的可能性。(C语言的互操作性在C++早期发展阶段是一个相当重要的卖点。)

      1. C++异常处理的设计灵感来源于ML语言,后者使用了“raise”一词。如果“signal”一词未被C标准库中的函数占用,C++本可能直接采用“raise”作为异常处理术语。“throw”和“catch”这两个词是由Maclisp引入的,这也是Stroustrup了解它们的原因。正如他在1993年第二届ACM编程语言历史(HOPL)会议上告诉Guy Steele的那样,“我认为我了解几乎所有使用异常的语言中所使用的名称,而‘throw’和‘catch’只是我最喜欢的那些。”

    3. 我认为“raise”一词源于异常会沿调用栈“向上”传播,将异常处理委托给上一层。而“throw”可能与“不知道如何处理错误情况,只好将其抛出(或无奈地摊开双手 xD)”的观念有关。完全只是猜测

      1. 我怀疑这源于“拉起”旗帜/信号(字面意思就像将旗帜拉上旗杆一样)来指示CPU状态,随后这种术语从硬件传播到软件。

        1. 我不知道,因为有些圈子里布尔变量被称为“标志”,但我从未见过它们被描述为“升起”或“降下”,只见过“设置”和“清除”。

        2. 听起来有道理。最早的异常处理系统中,CPU异常和软件异常在语义上没有区别。

          1. 你仍然可以使用SIGUSR1和SIGUSR2来实现。

        3. 我以为这源自“提出问题”或“大闹一场”的概念。

    4. 你抛出一个可捕获的异常,如果未能捕获它,系统就会崩溃。除非它是一颗钢球。

      你抛出标志或问题,这些都是异常的良好描述。

    5. 这是一个很好的问题。我学习的第一门编程语言是Python,“for i in range(10)”对我来说很有意义。但“for (int i = 0; i < 10; i++)”肯定先出现,这种情况下“for”就不是那么显而易见了。

      1. BASIC在1964年就有了FOR-NEXT循环。

        10 FOR N = 1 TO 10

        20 PRINT “ ”;

        30 NEXT N

        C语言于1972年首次发布,其for循环包含赋值、条件和增量三个部分。

        1. 这让我想起一个小趣闻。在非常早期的BASIC版本中,“FORD=STOP”会被解析为“FOR D = S TO P”。

          我大约在1975年觉得这很有趣。

          1. Fortran随着时间推移发展了很多。如果有人在196X年说它没有do循环,我也不会太惊讶。

            其实这只是语法糖,只需使用goto即可。

            1. FORTRAN IV,至少我在PDP-11上运行RSX时使用的版本,没有DO循环。只有IF和GO TO。但它同时支持逻辑IF和算术IF。

            2. FORTRAN的整个设计初衷就是作为一个有效的优化编译器来处理DO循环。

      2. FOR来自ALGOL,据我所知,它的拼写是:

            for p := x step d until n do
        
      3. ALGOL 58中有“for i:=0(1)9”。C语言的for循环是更通用的变体。

    6. “for”循环语句与数学术语相符:“对于集合[1:20]中的每个整数i,…”

    7. “for”可能是“for each”的缩写。`for i in 1..=10`相当于“对于每个整数i在1到10的范围内”。

  19. 为什么杰克·伦敦的小说中那只狗被称为“wild”?

  20. 因为,显然,我们在代码段中突出显示并喊出要跳转到的地址或要压入栈中的地址。;)

  21. 将此与“调用”或“应用”(例如调用或应用)进行对比也颇有趣味。我敢肯定,市面上肯定有不少“函数式方言”!

  22. > 调用函数就像召唤仆人——一种执行任务的召唤。

    所以我们把 Git 分支从 master 改名为 main……因为殖民主义。

    那么正确的非殖民主义词汇是什么?请求、要求、恳求?

    这里有些人似乎喜欢“召唤”这个词。唉,唉,唉

  23. 因为它们是子程序。你需要调用它们。通常使用调用指令。

  24. 我记得以前人们会说“调用它”来请求操作员在计算机上执行一个函数,而结果会显示在用户面前。

  25. 格伦多尔:我能召唤深渊中的幽灵。

    霍特斯珀:为什么,我也能,任何人都能;但当你召唤他们时,他们会来吗?

    — 《亨利四世》第一部

  26. 还存在:“激活”来自激活记录

  27. 你“调用”函数来执行任务,或返回值,具体情况而定。就像你可能召唤一个仆人或其他什么一样。

    1. 答案在文章中。你“调用”函数是因为它们存储在库中,就像书一样,而就像图书馆里的书一样,当你需要它们时,你通过指定其“调用编号”来识别你想要的那个。

      1. 文章中另一个答案是,你调用它们就像你叫医生到家里一样,让他们为你做点什么,或者就像人们“待命”一样。

        那个故事中的“索书号”是在“调用”之后出现的。不是相反的顺序。

  28. 我想指出CALL这条CPU指令及其起源。我对此不太熟悉,但它可能揭示出比编程语言更多的信息。这条指令至少从第一代英特尔微处理器和微控制器设计时就已存在。

    1. 我一直往下滚动,期待看到这个故事:

      > Dennis Ritchie通过告诉所有人,C语言中的函数调用非常、非常便宜,从而鼓励了模块化。大家都开始编写小型函数并进行模块化。多年后我们发现,在PDP-11上函数调用仍然很昂贵,而VAX代码经常有50%的时间花在CALL指令上。Dennis骗了我们!但为时已晚;我们都上瘾了…

      https://www.catb.org/~esr/writings/taoup/html/modularitychap…

    2. 第一款英特尔微控制器是8008和4004,设计于1971年。这比本文记录的1958年FORTRAN II中的“CALL X”语句晚了13年,此后(他记录道)该术语迅速普及。(FORTRAN I没有子程序。)

      在Algol 58报告https://www.softwarepreservation.org/projects/ALGOL/report/A…中,我们有“过程”和“函数”作为子程序的类型。关于调用过程,报告中提到:

      > 9. 过程语句

      > 过程语句用于启动(调用)过程的执行,过程是一个封闭且自包含的流程,具有固定的输入和输出参数顺序,由过程声明永久定义。(参见过程声明。)

      需注意,此处特意使用了“调用”一词,但并未采用“调用过程”的表述。实际上,它调用的并非过程本身,而是过程的执行。

      然而,它也使用“过程调用”一词来描述过程的启动或执行:

      > 定义被调用过程的过程声明在其标题中包含与过程语句形式相同的符号串,而占据输入和输出参数位置的形式参数提供了关于任何过程调用中使用的参数有效性的完整信息,(…)

      Algol 58定义函数的结构与过程不同,但函数同样通过“函数调用”调用——而非“调用函数”。

      我不确定首款包含“调用”指令的汇编语言何时出现,但可能早于1958年。Burroughs 5000似乎是个值得研究的对象。但当时许多汇编语言并未采用此设计;甚至MIX也使用STJ指令在被调用子程序的返回指令中设置返回地址,然后直接跳转至其入口点,而PDP-10据我所记得使用PUSHJ指令。360使用BALR(分支和链接寄存器),与RISC-V今天的JALR指令类似。

发表回复

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

你也许感兴趣的: