跳转到内容

Sway 参考手册/关于函数的更多内容

来自维基教科书,开放世界中的开放书籍

我们已经看到了一些函数的例子,有些是用户定义的,有些是内置的。例如,我们使用了内置函数,例如*实际上,*不是一个函数,而是一个绑定到将两个数字相乘的函数的变量,但说“绑定到*的函数”很繁琐,所以我们用更简洁(但技术上不正确)的短语“the*函数”。

预定义函数

[编辑 | 编辑源代码]

Sway 有许多预定义函数。你可以通过执行以下命令查看内置函数列表

   sway -p

在系统提示符(不要将系统提示符与 Sway 解释器提示符混淆)处。关于内置函数的更多详细信息将在最后一章给出。然而,没有人能够预料到人们可能想要执行的所有可能的任务,因此大多数编程语言允许用户定义新的函数。Sway 也不例外,它提供了创建新的和新颖的函数。当然,为了有用,这些函数应该能够调用内置函数以及其他程序员创建的函数。

例如,一个确定给定数字是奇数还是偶数的函数没有内置在 Sway 中,但在某些情况下非常有用。以下是如何定义一个名为 even? 的函数,如果给定的数字是偶数则返回 true,否则返回 false

   sway> function even?(x) { return x % 2 == 0; }
   FUNCTION: <function even?(x)>
    
   sway> even?;
   FUNCTION: <function even?(x)>
    
   sway> even?(4);
   SYMBOL: :true
    
   sway> even?(5);
   SYMBOL: :false
    
   sway> even?(4 + 5);
   SYMBOL: :false

我们可以谈论几天关于这些与解释器的交互中发生了什么。首先,让我们谈谈函数定义的语法。稍后,我们将讨论函数定义的目的。最后,我们将讨论函数定义和函数调用的机制。

函数语法

[编辑 | 编辑源代码]

回想一下,编程语言的词汇包括它的基本类型、关键字和变量。函数定义对应于语言中的一个句子,因为它是由语言的词语构成的。与人类语言一样,句子必须遵循某种形式。这种对句子形式的规范被称为它的 语法。计算机科学家经常使用一种特殊的方式来描述编程语言的语法,称为 Backus-Naur 形式 (BNF)。以下是使用 BNF 描述 Sway 函数定义语法的概括

   functionDefinition :
       'function' variable '(' optionalParameterList ')' block
   
   optionalParameterList : 
                         | parameterList
   
   parameterList : variable
                 | variable ',' parameterList
   
   block: '{' definitionSequence statementSequence '}'

第一个 BNF 规则说,函数定义以关键字 function(逐字出现的规则部分出现在单引号内)开头,后跟一个变量,后跟一个左括号,后跟一个名为 optionalParameterList 的东西,后跟一个右括号,后跟一个名为 block 的东西。通过阅读剩余的规则,我们看到,通过输入关键字 function 后跟函数的名称,后跟一个包含形式参数的括号列表(可能为空,如 所示),后跟函数体来定义函数。函数体是一个用大括号括起来的定义列表,然后是语句(参数列表后面的块通常称为 函数体)。

参数列表由零个或多个变量名组成,用逗号隔开。参数是局部变量,将被绑定到函数调用中给定的值。在 even? 的特定情况下,变量 x 将绑定到要确定其奇偶性的数字。习惯上称 x 为函数 even? 的 形式参数。在函数调用中,绑定到形式参数的值称为 参数

函数对象

[编辑 | 编辑源代码]

让我们看看 even? 的主体。 % 运算符绑定到余数或模运算符。== 运算符绑定到相等函数,并确定左操作数表达式的值是否等于右操作数表达式的值,并根据需要生成 true 或 false。然后,== 生成的值立即作为函数的值返回。

当给定一个像上面的函数定义时,Sway 会执行几个任务。第一个是创建函数的内部形式,称为函数对象,它包含函数的名称、参数列表和主体,以及当前环境。第二个任务是将函数名称和函数对象添加到当前环境中,作为一个变量-值绑定。因此,函数的名称只是一个恰好绑定到函数对象的变量。如前所述,我们经常说“函数 even?”,即使我们实际上指的是“绑定到变量 even? 的函数”。

函数定义的值是函数对象,其类型为 FUNCTION(表示 Sway 的用户已定义了函数)和可打印值为 <function definitionName formalParameters>,其中 definitionName 是创建函数时的函数名称,而 formalParameters 是参数的括号列表。虽然函数对象的打印值只列出了原始名称和参数,但实际对象包含它创建的上下文(称为定义环境)以及主体。

我们可以通过将函数对象传递给内置 ppObject 函数来查看函数对象的实际组成部分。ppObject 中的 pp 代表 漂亮打印,这意味着以一种令人愉悦的格式显示。

   sway> ppObject(even?);
   <FUNCTION 2421>:
       context: <OBJECT 749>
       prior: :null
       filter: :null
       parameters: (x)
       code: { return x % 2 == 0; }
       name: even?
   FUNCTION: <function even?(x)>

除了我们将在稍后了解的一些其他字段之外,我们看到 parameterscodename 字段看起来与预期一致。唯一意外的项目是代码包含一个 return,而之前没有。你将在后面的章节中了解更多关于返回值的信息。现在,我们只说返回值允许你从函数体中的最后一个表达式以外的其他地方返回一个值。

既然我们正在谈论漂亮打印,我们也可以使用 pp 函数查看 even?

   sway> pp(even?)
   function even?(x)
       {
       return x % 2 == 0;
       }
   FUNCTION: <function even?(x)>

调用函数

[编辑 | 编辑源代码]

一旦创建了函数,它就可以通过使用 参数 调用 该函数来使用。函数调用通过提供函数的名称,后跟一个包含表达式列表的括号、逗号分隔列表来实现。参数是这些表达式的值,并将绑定到形式参数。通常,如果存在 n 个形式参数,则应该存在 n 个参数[1]。此外,第一个参数的值绑定到第一个形式参数,第二个参数绑定到第二个形式参数,依此类推。此外,参数通常在绑定到参数之前进行评估[2]

一旦评估后的参数绑定到参数,函数体就会被评估。大多数时候,函数体中的表达式会引用参数。如果是这样,解释器是如何找到这些参数的值的呢?这个问题将在下一节中得到解答。

函数的形式参数可以看作是只有在评估函数体时才有效的变量定义。也就是说,这些变量只有在函数体中可见,其他地方不可见。因此,参数被认为是局部变量定义,因为它们只有局部效果(函数体)。任何对函数体之外的这些特定变量的直接引用都是不允许的[3]。考虑以下与解释器的交互

   sway> function square(a) { return a * a; }
   FUNCTION: <function square(a)>
    
   sway> square(4);
   INTEGER: 16
    
   sway> a;
   EVALUATION ERROR: :undefinedVariable
   variable a is undefined
       stdin,line 3: a;

范围是指变量可见的地方。

在上面的示例中,变量 a 的范围仅限于函数 square 的主体。任何对 a 的引用,除了在 square 的上下文中,都是无效的。现在考虑与解释器的另一个稍微不同的交互

   sway> var a = 10;
   INTEGER: 10
    
   sway> var b = 1;
   INTEGER: 1
    
   sway> function almostSquare(a) { return a * a + b; }
   FUNCTION: <function almostSquare(a)>
    
   sway> almostSquare(4);
   INTEGER: 17

在此对话中,两个变量定义 abalmostSquare 的定义之前。此外,充当 almostSquare 形式参数的变量与对话中定义的第一个变量同名。此外,almostSquare 的主体引用了变量 a 和 b。变量 a 被定义了两次(一次作为普通变量,一次作为形式参数),而变量 b 被引用,但不是形式参数。虽然乍一看很令人困惑,但 Sway 解释器毫不费力地弄清楚了这一切。从解释器的响应来看,函数体中的 b 必须引用最初值为 1 的变量(因为它是唯一的 b)。函数体中的 a 必须引用形式参数,其值通过调用函数设置为 4(根据解释器的输出)。

当一个形式参数(或任何函数内部的局部定义)与另一个也在作用域内的变量同名时,我们就说形式参数遮蔽了另一个变量。术语“遮蔽”是指另一个变量处于形式参数的阴影之中,不可见。如果一个变量在当前环境中或在当前环境中绑定到上下文变量的环境中绑定,则该变量被称为在作用域内。我们可以通过观察 Sway 中的绑定清楚地看到这一点。考虑这个对话

   sway> var a = 10;
   INTEGER: 10
    
   sway> var b = 1;
   INTEGER: 1
    
   sway> function almostSquare(a) { pp(this); a * a + b; }
   FUNCTION: <function almostSquare(a)>

请注意,在这个版本的almostSquare函数体中,我们对当前环境进行了漂亮打印。

   sway> almostSquare(4);
   <OBJECT 2569>:
       context: <OBJECT 749>
       dynamicContext: <OBJECT 749>
       callDepth: 1
       constructor: <function almostSquare(a)>
       this: <OBJECT 2569>
       a: 4
   INTEGER: 17

在这个当前环境中,a的值被用来计算返回值。我们看到,实际上,a的值为 4。但b的值在哪里找到呢?

当需要一个变量的值时,Sway 会在当前环境(this)中查找。如果在那里找不到,Sway 会在context中查找。如果变量不在context中,它会在contextcontext中查找,依此类推。让我们修改almostSquare来进行说明

   sway> function almostSquare(a) { pp(this); pp(context); a * a + b; }
   FUNCTION: <function almostSquare(a)>

这个最新版本对它的当前环境及其上下文进行了漂亮打印

   sway> almostSquare(4);
   <OBJECT 2603>:
       context: <OBJECT 749>
       dynamicContext: <OBJECT 749>
       callDepth: 1
       constructor: <function almostSquare(a)>
       this: <OBJECT 2603>
       a: 4
   <OBJECT 749>:
       context: <OBJECT 18>
       dynamicContext: :null
       callDepth: 0
       constructor: :null
       this: <OBJECT 749>
       almostSquare: <function almostSquare(a)>
       b: 1
       a: 10
       SwayEnv: ["SSH_AGENT_PID=5076","SHELL=/bin/bash","TERM=x...
       SwayArgs: :null
   INTEGER: 17

这里我们看到了abalmostSquare的绑定,正如预期的那样。

这表明了两个事情

  1. 在函数体内,形式参数在当前环境中找到
  2. 函数调用中活动的环境的上下文被绑定到函数的定义环境。

当对a进行引用时,会在当前环境中进行搜索。在函数体内,立即找到了值为 4 的值。当需要b的值时,它在当前环境中找不到。然后解释器搜索当前环境的上下文(在本例中为 <OBJECT 1616>)。该对象确实对b进行了绑定。

由于a的值为 4,b的值为 1,函数返回的值为 17。最后,与解释器的最后一次交互说明了a的初始绑定没有受到函数调用的影响。

一般来说,在当前环境中找到的变量被认为是局部变量。作用域内但不在当前环境中的变量被认为是非局部变量。或者,局部变量被认为驻留在局部作用域中,而非局部变量被认为驻留在非局部作用域中。局部作用域和非局部作用域有时分别被称为内部作用域和外部作用域。如果一个非局部变量驻留在最外层作用域,它被认为是全局变量,保存全局变量绑定的环境被称为全局环境。在 Sway 中,初始环境是全局环境。与上面的定义相反,Sway 的全局环境确实有一个外部作用域;这个外部作用域保存了内置函数的绑定。内置环境是不可修改的,所以全局环境更好的定义可能是最外层的环境,可以被程序员修改。

环境 <OBJECT 1845>,即函数体在其中执行的环境是如何产生的呢?表达式 almostSquare(4) 的求值会触发一系列动作。第一个动作是创建一个新的环境,该环境将保存要调用的函数的形式参数(在本例中,唯一的参数为 a),并将这些参数绑定到调用中相应参数的值(在本例中,唯一的参数的值为 4)。这个新环境的上下文变量绑定到与要调用的函数相关联的函数对象中找到的上下文变量(这个新环境也包含其他预定义的变量,如构造函数)。然后在该新环境下执行要调用的函数体。创建新环境并通过其上下文变量将它的上下文链接到另一个环境的过程被称为扩展环境。总而言之,当执行函数调用时,会执行以下动作

  • 函数调用的参数在当前环境下求值
  • 从当前环境中检索与要调用的函数相关联的函数对象
  • 从函数对象中检索形式参数
  • 从函数对象中检索定义环境
  • 从定义环境扩展一个新的环境
  • 通过将形式参数绑定到已求值的参数来填充新环境
  • 从函数对象中检索函数体
  • 在新建的扩展环境下求值函数体
  • 将该求值的结果作为函数调用的结果返回

关于函数调用有两个重要的概念:静态链和动态链。静态链我们已经看到了,即当前环境、当前环境的上下文、上下文的上下文,等等。这个链就是用来搜索被引用变量的值的。而另一方面,动态链是当前环境、调用函数的环境、调用函数的调用者的环境,等等。与大多数语言不同的是,动态链可供程序员搜索和操作。目前,这种操作超出了我们的能力,因此我们将把这个话题推迟到以后再讨论。

从函数返回

[edit | edit source]

函数的返回值是在执行函数体时求值的最后一个表达式的值。return函数用于使函数体中的任何表达式成为最后一个求值的表达式。

   function test(x,y)
       {
       if (y == 0)
           {
           return(0);
           }
       println("good value for y!");
       return (x / y);
       }

在这个例子中,如果 y 为零,则立即从函数中返回,并且不求值函数体中的其他表达式。如果不是,则打印一条消息并返回商。最后的返回实际上是不必要的;只要有x / y作为最后一个表达式,它也能工作。

为了使 Sway,一种函数式语言,看起来像 C 和 Java,对return函数的调用有另一种语法,括号包围的单个参数可以省略

   function test(x,y)
       {
       if (y == 0) { return 0; }
       println("good value for y!");
       x / y;
       }

脚注

[edit | edit source]
  1. 对于可变参数函数,参数的数量可能超过形式参数的数量。
  2. 可以延迟参数的求值。有关更多详细信息,请参见延迟求值的章节。
  3. 有时可以间接地访问这些变量,在 对象的情况下。


将代码存储在文件中 · 调试

华夏公益教科书