基本的函數定義與調用執行、引入閉包使Stone語言能夠將變量賦值爲函數,或將函數做爲參數傳遞給其餘函數 有些函數將有返回值的歸爲函數,沒有返回值的歸爲子程序java
此書將函數定義語句稱爲def語句。def語句僅能用於最外層代碼,用戶沒法在代碼塊中定義函數閉包
Stone語言將最後執行語句(表達式)的計算結果將做爲函數的返回值返回ide
param : IDENTIFIER params : param { "," param } param_list : "(" [ params ] ")" def : "def" IDENTIFIER param_list block args : expr { "," expr } postfix : "(" [ args ] ")" primary : ( "(" expr ")" | NUMBER | IDENTIFIER | STRING ) { postfix } simple : expr [ args ] program : [ def | statement ] (";" | EOL)
形參param是一種標識符(變量名)。形參序列params至少包含一個param,各個參數之間經過逗號分隔。 param_list能夠是以括號括起的params,也能夠是空括號對()。 函數定義語句def由def、標識符(函數名)、param_list及block組成。 實參args由若干個經過逗號分隔的expr組成。 postfix能夠是以括號括起的args,也能夠是省略了args的空括號對函數
非終結符primary須要在原有基礎上增長對錶達式中含有的函數調用的支持。所以,本章修改了代碼清單5.1中primary的定義。在原先的primary以後增長若干個(能夠爲0)postfix(後綴)獲得的依然是一個primary。這裏的postfix是用括號括起的實參序列post
此外,表達式語句simple也須要支持函數調用語句。所以,本章修改了以前的定義,使simple不只能由expr組成,expr後接args的組合也是一種simple語句測試
與primary不一樣,simple不支持由括號括起的實參args。也就是說ui
simple : expr [ "(" [ args ] ")" ]
是不正確的,應該使用下面的形式this
simple : expr [ args ]
代碼清單7.2是根據代碼清單7.1的語法規則設計的語法分析程序。其中FuncParser類繼承於第5章代碼清單5.2中的Basicparser類。也就是說,語法分析器的基本部分利用了Basicparser類中已有的代碼,FuncParser類僅定義了新增的功能。和以前同樣,新定義的非終結符也經過parser庫實現。代碼清單7.三、代碼清單7.4與代碼清單7.5是更新後的抽象語法樹的節點類lua
代碼清單7.2中,paramList字段與postfix字段的初始化表達式使用了maybe方法。 例如,paramList字段的定義以下所示設計
Parser paramList = rule().sep("(").maybe(params).sep(")");
與option方法同樣,maybe方法也用於向模式中添加可省略的非終結符。 paramList字段對應的非終結符param_list實際的語法規則以下所示
param_list : "(" [ params ] ")"
省略params建立的子樹是一棵以ParameterList對象爲根節點的樹。根節點是該子樹惟一的節點,這棵子樹除根節點外沒有其餘子節點。parameterList(參數列表)對象的子節點本來用於表示參數,params被省略時,根節點的子節點數爲0,恰巧可以很好地表示沒有參數
即便params被省略,抽象語法樹仍將包含一個params的子樹來表示這個實際不存在的成分。根據第5章介紹的特殊規定,爲了不建立沒必要要的節點,與params對應的子樹將直接做爲與非終結符param_list對應的子樹使用
非終結符定義的修改由構造函數完成。構造函數首先須要爲reserved添加右括號),以避免將它識別爲標識符。以後,primary與simple模式的末尾也要添加非終結符,爲此須要根據相應的字段調用合適的方法。例如,simp1e字段應調用option方法
simple.option(args)
經過這種方式,option方法將在由Basicparser類初始化的simple模式末尾添加一段新的模式。也就是說,BasicParser在進行初始化時,將再也不執行下面的語句
Parser simple = rule(PrimaryExpr.class).ast(expr);
而執行下面的代碼
Parser simple = rule(PrimaryExpr.class).ast(expr).option(args)
構造函數的最後一行調用了program字段的insertChoice方法,將用於表示def語句的非終結符def添加到了program中。該方法將把def做爲or的分支選項,添加到與program對應的模式以前
經過insertChoice方法添加def以後,program表示的模式將與下面定義等價
Parser program = rule().or(def,statement,rule(NullStmnt.class)).sep(";",Token.EOL)
算上def,表達式中or的分支選項增長到了3個。新增的選項和原有的兩個同樣,都是or方法的直接分支,語法分析器在執行語句時必須首先判斷究競選擇哪一個分支
package stone; import static Stone.Parser.rule; import stone.ast.ParameterList; import stone.ast.Arguments; import stone.ast.DefStmnt; public class FuncParser extends BasicParser { Parser param = rule().identifier(reserved); Parser params = rule(ParameterList.class) .ast(param).repeat(rule().sep(",").ast(param)); Parser paramList = rule().sep("(").maybe(params).sep(")"); Parser def = rule(DefStmnt.class) .sep("def").identifier(reserved).ast(paramList).ast(block); Parser args = rule(Arguments.class) .ast(expr).repeat(rule().sep(",").ast(expr)); Parser postfix = rule().sep("(").maybe(args).sep(")"); public FuncParser() { reserved.add(")"); primary.repeat(postfix); simple.option(args); program.insertChoice(def); } }
package stone.ast; import java.util.List; public class ParameterList extends ASTList { public ParameterList(List<ASTree> list) { super(list); } public String name(int i) { return ((ASTLeaf) child(i)).token().getText(); } public int size() { return numChildren(); } }
package stone.ast; import java.util.List; public class DefStmnt extends ASTList { public DefStmnt(List<ASTree> list) { super(list); } public String name() { return ((ASTLeaf) child(0)).token().getText(); } public ParameterList parameters() { return (ParameterList) child(1); } public BlockStmnt body() { return (BlockStmnt) child(2); } public String toString() { return "(def )" + name() + " " + parameters() + " " + body() + ")"; } }
package stone.ast; import java.util.List; public class Arguments extends Postfix { public Arguments(List<ASTree> c) { super(c); } public int size() { return numChildren(); } }
環境是變量名與變量的值的對應關係表。大部分程序設計語言都支持僅在函數內部有效的局部變量。爲了讓Stone語言也支持局部變量,咱們必須從新設計環境
變量的做用域是指該變量能在程序中有效訪問的範圍。例如,Java語言中方法的參數只能在方法內部引用。也就是說,一個方法的參數的做用域限定於該方法內部。
變量的生存週期則是該變量存在的時間期限。例如,Java語言中某個方法的參數p的生存週期就是該方法的執行期。換言之,參數p在方法執行過程當中將始終有效。若是該方法中途調用了其餘方法,就會離開原方法的做用域,新調用的方法沒法引用原方法中的參數p。不過,雖然參數p此時沒法引用,它仍會繼續存在,保存當前值。當程序返回原來的方法後,又回到了參數p的做用域,將可以再次引用參數p。引用參數p獲得的天然是它原來的值。方法執行結束後,參數p的生存週期也將一同結束,參數p再也不有效,環境中保存的相應名值對也不復存在。事實上,環境也沒有必要繼續保持該名值對。以後若是程序再次調用該方法,參數p將與新的值(實參)關聯
一般,變量的做用域由嵌套結構實現。Stone語言支持在整個程序中都有效的全局變量做用域及僅在函數內部有效的局部變量與函數參數做用域
爲表現嵌套結構,咱們須要爲每一種做用域準備一個單獨的環境,並根據須要嵌套環境。在查找變量時,程序將首先查找與最內層做用域對應的環境,若是沒有找到,再接着向外逐層查找。目前的Stone語言尚不支持在函數內定義函數,所以僅有兩種做用域,即全局變量做用域及局部變量做用域。而在支持函數內定義函數的語言中,可能存在多層環境嵌套
Java等一些語言中,大括號{}括起的代碼塊也具備獨立的做用域。代碼塊中聲明的變量只能在該代碼塊內部引用。Stone語言目前沒有爲代碼塊設計專門的做用域,以後也不會爲每一個代碼塊提供單獨的做用域。所以,Stone語言將始終僅有兩個做用域。
package chap7; import chap6.Environment; import java.util.HashMap; import chap7.FuncEvaluator.EnvEx; public class NestedEnv implements Environment { protected HashMap<String, Object> values; protected Environment outer; public NestedEnv() { this(null); } public NestedEnv(Environment e) { values = new HashMap<String, Object>(); outer = e; } public void setOuter(Environment e) { outer = e; } public void put(String name, Object value) { Environment e = where(name); if (e == null) e = this; ((EnvEx)e).putNew(name,value) } public void putNew(String name, Object value) { values.put(name, value); } public Environment where(String name) { if (values.get(name) != null) return this; else if (outer == null) return null; return ((EnvEx) outer).where(name); } public Object get(String name) { Object v = values.get(name); if (v == null && outer != null) return outer.get(name); return v; } }
爲了使環境支持嵌套結構,須要從新定義了Environment接口的類實現。代碼清單7.6是從此須要使用的NestedEnv類的定義
與BasicEnv類不一樣,NestedEnv類除了value字段,還有一個outer字段。該字段引用的是與外側一層做用域對應的環境。此外,get方法也須要作相應的修改,以便查找與外層做用域對應的環境。爲確保put方法可以正確更新變量的值,咱們也須要對它作修改。若是當前環境中不存在參數指定的變量名稱,而外層做用域中含有該名稱,put方法應當將值賦給外層做用域中的變量。爲此,咱們須要使用輔助方法where。該方法將查找包含指定變量名的環境並返回。若是全部環境中都不含該變量名,where方法將返回nul1
NestedEnv類提供了一個putNew方法。該方法的做用與BasicEnv類的put方法相同。它在賦值時不會考慮outer字段引用的外層做用域環境。不管外層做用域對應的環境中是否存在指定的變量名,只要當前環境中沒有該變量,putNew方法就會新增一個變量。
此外,爲了能讓NestedEnv類的方法經由Environment接口訪問,咱們須要向Environment接口中添加一些新的方法。代碼清單7.7定義的FuncEvaluator修改器定義了一個EnvEx修改器,它添加了這些新的方法。
爲了讓解釋器可以執行函數,必須爲抽象語法樹的節點類添加eva1方法。這由代碼清單7.7的FuncEvaluator修改器實現。
函數的執行分爲定義與調用兩部分。程序在經過def語句定義函數時,將建立用於表示該函數的對象,向環境添加該函數的名稱並與該對象關聯。也就是說,程序會向環境添加一個變量,它以該對象爲變量值,以函數名爲變量名。函數由Function對象表示。代碼清單7.8定義了Function類。
代碼清單7.7的FuncEvaluator修改器包含多個子修改器。其中,DefstmntEX修改器用於向 Defstmnt類添加eva1方法。
PrimaryEx修改器將向PrimaryExpr類添加方法。函數調用表達式的抽象語法樹與非終結符primary對應。非終結符primary本來只表示字面量與變量名等最基本的表達式成分,如今,咱們修改它的定義,使函數調用表達式也能被判斷爲一種primary。即primary將涵蓋由primary後接括號括起的實參序列構成的表達式,下圖是一個例子,它是由函數調用語句fact(9)構成的抽象語法樹。爲了支持這一修改,咱們須要爲PrimaryExpr類添加若干新方法。
operand方法將返回非終結符primary原先表示的字面量與函數名等內容,或返回函數名稱。postfix方法返回的是實參序列(若存在)。eval方法將首先調用operand方法返回的對象的eval方法。若是函數存在實參序列,eval方法將把他們做爲參數,進一步調用postfix方法(在上圖中即Arguments對象)返回的對象的eval方法
PrimaryExpr類新增的postfix方法的返回值爲Postfix類型。Postfix是一個抽象類(代碼清單7.9),它的子類Arguments類是一個用於表示實參序列的具體類。ArgumentsEx修改器爲Arguments類添加的eva1方法將實現函數的執行功能
Arguments類新增的eval方法是函數調用功能的核心。它的第2個參數value是與函數名對應的抽象語法樹的eval方法的調用結果。但願調用的函數的Function對象將做爲value參數傳遞給eval方法。Function對象由def語句建立。函數名與變量名的處理方式相同,所以解釋器僅需調用eval方法就能從環境中獲取Function對象
以後,解釋器將以環境callerEnv爲實參計算函數的執行結果。首先,Function對象的parameters 方法將得到形參序列,實參序列則由自身提供iterator方法獲取。而後解釋器將根據實參的排列順序依次調用eval並計算求值,將計算結果與相應的形參名成對添加至環境中。ParameterList類新增的eval方法將執行實際的處理
實參的值將被添加到新建立的用於執行函數調用的newEnv環境,而非callerEnv環境(表7.1)。newEnv環境表示的做用域爲函數內部。若是函數使用了局部變量,它們將被添加到該環境
environment | mean |
---|---|
newEnv | 調用函數時新建立的環境。用於記錄函數的參數及函數內部使用的局部變量 |
newEnv.outer | newEnv的outer字段引用的環境,可以表示函數外層做用域。該環境一般用於記錄全局變量 |
callerEnv | 函數調用語句所處的環境。用於計算實參 |
最後,Arguments類的eval方法將在新建立的環境中執行函數體。函數體能夠經過調用Function對象的body方法得到。函數體是def語句中由大括號{}括起的部分,body方法將返回與之對應的抽象語法樹。調用返回的對象的eva1方法便可執行該函數
用於調用函數的環境newEnv將在函數被調用時建立,在函數執行結束後捨棄。這與函數的參數及局部變量的生存週期相符。若解釋器屢次遞歸調用同一個函數,它將在每次調用時建立新的環境。只有這樣才能正確執行函數的遞歸調用
有時,用於計算實參的環境callerEnv與執行def語句的是同一個環境,但也並不是老是如此。callerEnv是用於計算調用了函數的表達式的環境。若是在最外層代碼中調用函數,callerEnv環境將同時用於保存全局變量。然而,若是函數由其餘函數調用,callerEnv環境則將保存調用該函數的外層函數的局部變量。環境雖然支持嵌套結構,但該結構僅反映了函數定義時的做用域嵌套狀況。在函數調用其餘函數時,新建立的環境不會出如今這樣的嵌套結構中
package chap7; import java.util.List; import javassist.gluonj.*; import stone.StoneException; import stone.ast.*; import chap6.BasicEvaluator; import chap6.Environment; import chap6.BasicEvaluator.ASTreeEx; import chap6.BasicEvaluator.BlockEx; @Require(BasicEvaluator.class) @Reviser public class FuncEvaluator { @Reviser public static interface EnvEx extends Environment { void putNew(String name, Object value); Environment where(String name); void setOuter(Environment e); } @Reviser public static class DefStmntEx extends DefStmnt { public DefStmntEx(List<ASTree> c) { super(c); } public Object eval(Environment env) { ((EnvEx)env).putNew(name(), new Function(parameters(), body(), env)); return name(); } } @Reviser public static class PrimaryEx extends PrimaryExpr { public PrimaryEx(List<ASTree> c) { super(c); } public ASTree operand() { return child(0); } public Postfix postfix(int nest) { return (Postfix)child(numChildren() - nest - 1); } public boolean hasPostfix(int nest) { return numChildren() - nest > 1; } public Object eval(Environment env) { return evalSubExpr(env, 0); } public Object evalSubExpr(Environment env, int nest) { if (hasPostfix(nest)) { Object target = evalSubExpr(env, nest + 1); return ((PostfixEx)postfix(nest)).eval(env, target); } else return ((ASTreeEx)operand()).eval(env); } } @Reviser public static abstract class PostfixEx extends Postfix { public PostfixEx(List<ASTree> c) { super(c); } public abstract Object eval(Environment env, Object value); } @Reviser public static class ArgumentsEx extends Arguments { public ArgumentsEx(List<ASTree> c) { super(c); } public Object eval(Environment callerEnv, Object value) { if (!(value instanceof Function)) throw new StoneException("bad function", this); Function func = (Function)value; ParameterList params = func.parameters(); if (size() != params.size()) throw new StoneException("bad number of arguments", this); Environment newEnv = func.makeEnv(); int num = 0; for (ASTree a: this) ((ParamsEx)params).eval(newEnv, num++, ((ASTreeEx)a).eval(callerEnv)); return ((BlockEx)func.body()).eval(newEnv); } } @Reviser public static class ParamsEx extends ParameterList { public ParamsEx(List<ASTree> c) { super(c); } public void eval(Environment env, int index, Object value) { ((EnvEx)env).putNew(name(index), value); } } }
package chap7; import stone.ast.BlockStmnt; import stone.ast.ParameterList; import chap6.Environment; public class Function { protected ParameterList parameters; protected BlockStmnt body; protected Environment env; public Function(ParameterList parameters,BlockStmnt body,Environment env) { this.parameters = parameters; this.body = body; this.env = env; } public ParameterList parameters() { return parameters; } public BlockStmnt body() { return body; } public Environment makeEnv() { return new NestedEnv(env); } public String toString() { return "<fun:" + hashCode() + ">"; } }
package stone.ast; import java.util.List; public class Postfix extends ASTList { public Postfix(List<ASTree> list) { super(list); } }
至此,Stone語言已支持函數調用功能。 代碼清單7.10是解釋器的程序代碼, 代碼清單7.11是解釋器的啓動程序。 解釋器所處的環境並非一個BasicEnv對象,而是一個由啓動程序建立的NestedEnv對象
下面咱們以計算斐波那契數爲例測試一下函數調用功能。代碼清單7.12是由Stone語言寫成的斐波那契數計算程序。程序執行過程當中,將首先定義fib函數,並計算fib(10)的值。最後輸出以下結果
=> fib => 55
package chap6; import stone.*; import stone.ast.ASTree; import stone.ast.NullStmnt; public class BasicInterpreter { public static void main(String[] args) throws ParseException { run(new BasicParser(), new BasicEnv()); } public static void run(BasicParser bp, Environment env) throws ParseException { Lexer lexer = new Lexer(new CodeDialog()); while (lexer.peek(0) != Token.EOF) { ASTree t = bp.parse(lexer); if (!(t instanceof NullStmnt)) { Object r = ((BasicEvaluator.ASTreeEx)t).eval(env); System.out.println("=> " + r); } } } }
package chap7; import javassist.gluonj.util.Loader; public class FuncRunner { public static void main(String[] args) throws Throwable { Loader.run(FuncInterpreter.class, args, FuncEvaluator.class); } }
def fib(n) { if n < 2 { n } else { fib(n - 1) + fib(n - 2) } } fib(10)
簡單來說,閉包是一種特殊的函數,它能被賦值給一個變量,做爲參數傳遞至其餘函數。閉包既能在最外層代碼中定義,也能在其餘函數中定義。一般,閉包沒有名稱
若是Stone語言支持閉包,下面的程序將能正確運行
inc = fun (x) { x + 1 } inc(3)
這段代碼將建立一個新的函數,它的做用是返回一個比接收的參數大1的值。該參數將被賦值給變量inc。賦值給變量的就是一個閉包。inc並不是函數的名稱,事實上,這種函數沒有名稱。不過,程序可以經過inc(3)的形式,以3爲參數調用該函數。讀者能夠將其理解爲,程序從名爲inc的變量中得到了一個閉包,並以3爲參數調用這個閉包
代碼清單7.13是閉包的語法規則。該規則修改了primary,向其中添加了閉包的定義
primary : " fun " param_list block | 本來的primary定義
代碼清單7.14是支持閉包功能的語法分析器程序。它修改了非終結符primary的定義,使語法分析器可以解析由fun起始的閉包。代碼清單7.15的Fun類是用於表示閉包的抽象語法樹的節點類。
Fun類的eval方法經過代碼清單7.16的ClosureEvaluator修改器增長。與def語句的eval方法同樣,它也會建立一個Function對象。Function對象的構造函數須要接收一個env參數,它是定義了該閉包的表達式所處的執行環境。
def語句在建立Function對象後會向環境添加由該對象與函數名組成的鍵值對,而在建立閉包時,eval方法將直接返回該對象。這樣一來,Stone語言就能將函數賦值給某個變量,或將它做爲參數傳遞給另外一個函數,實現閉包的語法功能。這時,實際賦值給變量或傳遞給函數的就是新建立的Function 對象。
package Stone; import static Stone.Parser.rule; import Stone.ast.Fun; public class ClosureParser extends FuncParser { public ClosureParser() { primary.insertChoice(rule(Fun.class).sep("fun").ast(paramList).ast(block)); } }
package Stone.ast; import java.util.List; public class Fun extends ASTList { public Fun(List<ASTree> c) { super(c); } public ParameterList parameters() { return (ParameterList)child(0); } public BlockStmnt body() { return (BlockStmnt)child(1); } public String toString() { return "(fun " + parameters() + " " + body() + ")"; } }
package chap7; import java.util.List; import stone.ast.ASTree; import stone.ast.Fun; import chap6.Environment; import javassist.gluonj.*; @Require(FuncEvaluator.class) @Reviser public class ClosureEvaluator { @Reviser public static class FunEx extends Fun { public FunEx(List<ASTree> c) { super(c); } public Object eval(Environment env) { return new Function(parameters(),body(),env); } } }
package chap7; import stone.ClosureParser; import stone.ParseException; import chap6.BasicInterpreter; public class ClosureInterpreter extends BasicInterpreter { public static void main(String[] args) throws ParseException { run(new ClosureParser(),new NestedEnv()); } }
從具體實現的角度來看,整數值有Java語言的Integer對象表現,字符串由String對象表現,而新添加的函數則由Function對象表現。
代碼清單7.17是支持閉包功能的Stone語言解釋器。代碼清單7.18是相應的啓動程序
package chap7; import javassist.gluonj.util.Loader; public class ClosureRunner { public static void main(String[] args) throws Throwable { Loader.run(ClosureInterpreter.class, args, ClosureEvaluator.class); } }
雖然如今程序已經支持函數何閉包了,Stone語言和其餘不少變量無需聲明便可使用的語言同樣,但若是已經存在某個全局變量,就是沒法在建立同名變量,比方說下面的例子
x = 1 def foo (i) { x = i; x + 1 }
函數foo沒法建立名爲x的局部變量。函數中的x將引用第一行的全局變量x。若是調用foo(3),全局變量x的值就會是3,這可就麻煩了。想用的是局部變量,實際使用的是全局變量,這裏彷佛存在大量錯誤隱患。若是非要區分二者,只要更改定義,讓全局變量的變量名必須以$開始就好了