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)>
除了我們將在稍後瞭解的一些其他欄位之外,我們看到 parameters、code 和 name 欄位看起來與預期一致。唯一意外的專案是程式碼包含一個 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
在此對話中,兩個變數定義 a 和 b 在 almostSquare 的定義之前。此外,充當 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中,它會在context的context中查詢,依此類推。讓我們修改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
這裡我們看到了a、b和almostSquare的繫結,正如預期的那樣。
這表明了兩個事情
- 在函式體內,形式引數在當前環境中找到
- 函式呼叫中活動的環境的上下文被繫結到函式的定義環境。
當對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]