兩周自制腳本語言-第7天 添加函數(shù)功能

第7天 添加函數(shù)功能

基本的函數(shù)定義與調(diào)用執(zhí)行而昨、引入閉包使Stone語言可以將變量賦值為函數(shù)赦役,或?qū)⒑瘮?shù)作為參數(shù)傳遞給其他函數(shù)
有些函數(shù)將有返回值的歸為函數(shù),沒有返回值的歸為子程序

7.1 擴充語法規(guī)則

函數(shù)定義語句的語法規(guī)則

此書將函數(shù)定義語句稱為def語句。def語句僅能用于最外層代碼,用戶無法在代碼塊中定義函數(shù)

Stone語言將最后執(zhí)行語句(表達式)的計算結(jié)果將作為函數(shù)的返回值返回

代碼清單 7.1 與函數(shù)相關(guān)的語法規(guī)則

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,各個參數(shù)之間通過逗號分隔宣谈。
param_list可以是以括號括起的params愈犹,也可以是空括號對()键科。
函數(shù)定義語句def由def闻丑、標識符(函數(shù)名)、param_list及block組成勋颖。
實參args由若干個通過逗號分隔的expr組成嗦嗡。
postfix可以是以括號括起的args,也可以是省略了args的空括號對

非終結(jié)符primary需要在原有基礎(chǔ)上增加對表達式中含有的函數(shù)調(diào)用的支持饭玲。因此侥祭,本章修改了代碼清單5.1中primary的定義。在原先的primary之后增加若干個(可以為0)postfix(后綴)得到的依然是一個primary茄厘。這里的postfix是用括號括起的實參序列

此外矮冬,表達式語句simple也需要支持函數(shù)調(diào)用語句。因此次哈,本章修改了之前的定義胎署,使simple不僅能由expr組成,expr后接args的組合也是一種simple語句

與primary不同窑滞,simple不支持由括號括起的實參args琼牧。也就是說

simple : expr [ "("  [ args ] ")" ]

是不正確的,應(yīng)該使用下面的形式

simple : expr [ args ]

代碼清單7.2是根據(jù)代碼清單7.1的語法規(guī)則設(shè)計的語法分析程序哀卫。其中FuncParser類繼承于第5章代碼清單5.2中的Basicparser類巨坊。也就是說,語法分析器的基本部分利用了Basicparser類中已有的代碼此改,F(xiàn)uncParser類僅定義了新增的功能趾撵。和之前一樣,新定義的非終結(jié)符也通過parser庫實現(xiàn)共啃。代碼清單7.3鼓寺、代碼清單7.4與代碼清單7.5是更新后的抽象語法樹的節(jié)點類

代碼清單7.2中,paramList字段與postfix字段的初始化表達式使用了maybe方法勋磕。
例如妈候,paramList字段的定義如下所示

Parser paramList = rule().sep("(").maybe(params).sep(")");

與option方法一樣,maybe方法也用于向模式中添加可省略的非終結(jié)符挂滓。
paramList字段對應(yīng)的非終結(jié)符param_list實際的語法規(guī)則如下所示

param_list : "(" [ params ] ")"

省略params創(chuàng)建的子樹是一棵以ParameterList對象為根節(jié)點的樹苦银。根節(jié)點是該子樹唯一的節(jié)點,這棵子樹除根節(jié)點外沒有其他子節(jié)點。parameterList(參數(shù)列表)對象的子節(jié)點原本用于表示參數(shù)幔虏,params被省略時纺念,根節(jié)點的子節(jié)點數(shù)為0,恰巧能夠很好地表示沒有參數(shù)

即使params被省略想括,抽象語法樹仍將包含一個params的子樹來表示這個實際不存在的成分陷谱。根據(jù)第5章介紹的特殊規(guī)定,為了避免創(chuàng)建不必要的節(jié)點瑟蜈,與params對應(yīng)的子樹將直接作為與非終結(jié)符param_list對應(yīng)的子樹使用

非終結(jié)符定義的修改由構(gòu)造函數(shù)完成烟逊。構(gòu)造函數(shù)首先需要為reserved添加右括號),以免將它識別為標識符铺根。之后宪躯,primary與simple模式的末尾也要添加非終結(jié)符,為此需要根據(jù)相應(yīng)的字段調(diào)用合適的方法位迂。例如访雪,simp1e字段應(yīng)調(diào)用option方法

simple.option(args)

通過這種方式,option方法將在由Basicparser類初始化的simple模式末尾添加一段新的模式掂林。也就是說臣缀,BasicParser在進行初始化時,將不再執(zhí)行下面的語句

Parser simple = rule(PrimaryExpr.class).ast(expr);

而執(zhí)行下面的代碼

Parser simple = rule(PrimaryExpr.class).ast(expr).option(args)

構(gòu)造函數(shù)的最后一行調(diào)用了program字段的insertChoice方法泻帮,將用于表示def語句的非終結(jié)符def添加到了program中精置。該方法將把def作為or的分支選項,添加到與program對應(yīng)的模式之前

通過insertChoice方法添加def之后刑顺,program表示的模式將與下面定義等價

Parser program = rule().or(def,statement,rule(NullStmnt.class)).sep(";",Token.EOL)

算上def氯窍,表達式中or的分支選項增加到了3個。新增的選項和原有的兩個一樣蹲堂,都是or方法的直接分支狼讨,語法分析器在執(zhí)行語句時必須首先判斷究競選擇哪個分支

代碼清單7.2 支持函數(shù)功能的語法分析器FuncParser.java

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);
    }
}

代碼清單7.3 ParameterList.java

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();
    }
}

代碼清單7.4 DefStmnt.java

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() + ")";
    }
}

代碼清單7.5 Arguments.java

package stone.ast;
import java.util.List;

public class Arguments extends Postfix {
    public Arguments(List<ASTree> c) {
        super(c);
    }

    public int size() {
        return numChildren();
    }
}

7.2 作用域與生存周期

環(huán)境是變量名與變量的值的對應(yīng)關(guān)系表。大部分程序設(shè)計語言都支持僅在函數(shù)內(nèi)部有效的局部變量柒竞。為了讓Stone語言也支持局部變量政供,我們必須重新設(shè)計環(huán)境

設(shè)計環(huán)境

作用域(scope)

變量的作用域是指該變量能在程序中有效訪問的范圍。例如朽基,Java語言中方法的參數(shù)只能在方法內(nèi)部引用布隔。也就是說,一個方法的參數(shù)的作用域限定于該方法內(nèi)部稼虎。

生存周期(extent)

變量的生存周期則是該變量存在的時間期限衅檀。例如,Java語言中某個方法的參數(shù)p的生存周期就是該方法的執(zhí)行期霎俩。換言之哀军,參數(shù)p在方法執(zhí)行過程中將始終有效沉眶。如果該方法中途調(diào)用了其他方法,就會離開原方法的作用域杉适,新調(diào)用的方法無法引用原方法中的參數(shù)p谎倔。不過,雖然參數(shù)p此時無法引用猿推,它仍會繼續(xù)存在片习,保存當前值。當程序返回原來的方法后蹬叭,又回到了參數(shù)p的作用域藕咏,將能夠再次引用參數(shù)p。引用參數(shù)p得到的自然是它原來的值具垫。方法執(zhí)行結(jié)束后侈离,參數(shù)p的生存周期也將一同結(jié)束试幽,參數(shù)p不再有效筝蚕,環(huán)境中保存的相應(yīng)名值對也不復(fù)存在。事實上铺坞,環(huán)境也沒有必要繼續(xù)保持該名值對起宽。之后如果程序再次調(diào)用該方法,參數(shù)p將與新的值(實參)關(guān)聯(lián)

通常济榨,變量的作用域由嵌套結(jié)構(gòu)實現(xiàn)坯沪。Stone語言支持在整個程序中都有效的全局變量作用域及僅在函數(shù)內(nèi)部有效的局部變量與函數(shù)參數(shù)作用域

為表現(xiàn)嵌套結(jié)構(gòu),我們需要為每一種作用域準備一個單獨的環(huán)境擒滑,并根據(jù)需要嵌套環(huán)境髓窜。在查找變量時结蟋,程序?qū)⑹紫炔檎遗c最內(nèi)層作用域?qū)?yīng)的環(huán)境,如果沒有找到,再接著向外逐層查找崖媚。目前的Stone語言尚不支持在函數(shù)內(nèi)定義函數(shù),因此僅有兩種作用域睁宰,即全局變量作用域及局部變量作用域缰贝。而在支持函數(shù)內(nèi)定義函數(shù)的語言中,可能存在多層環(huán)境嵌套

Java等一些語言中柠衍,大括號{}括起的代碼塊也具有獨立的作用域洋满。代碼塊中聲明的變量只能在該代碼塊內(nèi)部引用。Stone語言目前沒有為代碼塊設(shè)計專門的作用域珍坊,之后也不會為每個代碼塊提供單獨的作用域牺勾。因此,Stone語言將始終僅有兩個作用域阵漏。

代碼清單7.6 NestedEnv.java

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;
    }
}

為了使環(huán)境支持嵌套結(jié)構(gòu)驻民,需要重新定義了Environment接口的類實現(xiàn)腺怯。代碼清單7.6是今后需要使用的NestedEnv類的定義

與BasicEnv類不同,NestedEnv類除了value字段川无,還有一個outer字段呛占。該字段引用的是與外側(cè)一層作用域?qū)?yīng)的環(huán)境。此外懦趋,get方法也需要做相應(yīng)的修改晾虑,以便查找與外層作用域?qū)?yīng)的環(huán)境。為確保put方法能夠正確更新變量的值仅叫,我們也需要對它做修改帜篇。如果當前環(huán)境中不存在參數(shù)指定的變量名稱,而外層作用域中含有該名稱诫咱,put方法應(yīng)當將值賦給外層作用域中的變量笙隙。為此,我們需要使用輔助方法where坎缭。該方法將查找包含指定變量名的環(huán)境并返回竟痰。如果所有環(huán)境中都不含該變量名,where方法將返回nul1

NestedEnv類提供了一個putNew方法掏呼。該方法的作用與BasicEnv類的put方法相同坏快。它在賦值時不會考慮outer字段引用的外層作用域環(huán)境。無論外層作用域?qū)?yīng)的環(huán)境中是否存在指定的變量名憎夷,只要當前環(huán)境中沒有該變量莽鸿,putNew方法就會新增一個變量。

此外拾给,為了能讓NestedEnv類的方法經(jīng)由Environment接口訪問祥得,我們需要向Environment接口中添加一些新的方法。代碼清單7.7定義的FuncEvaluator修改器定義了一個EnvEx修改器蒋得,它添加了這些新的方法级及。

7.3 執(zhí)行函數(shù)

為了讓解釋器能夠執(zhí)行函數(shù),必須為抽象語法樹的節(jié)點類添加eva1方法窄锅。這由代碼清單7.7的FuncEvaluator修改器實現(xiàn)创千。

函數(shù)的執(zhí)行分為定義與調(diào)用兩部分。程序在通過def語句定義函數(shù)時入偷,將創(chuàng)建用于表示該函數(shù)的對象追驴,向環(huán)境添加該函數(shù)的名稱并與該對象關(guān)聯(lián)。也就是說疏之,程序會向環(huán)境添加一個變量殿雪,它以該對象為變量值,以函數(shù)名為變量名锋爪。函數(shù)由Function對象表示丙曙。代碼清單7.8定義了Function類爸业。

代碼清單7.7的FuncEvaluator修改器包含多個子修改器。其中亏镰,DefstmntEX修改器用于向 Defstmnt類添加eva1方法扯旷。

PrimaryEx修改器將向PrimaryExpr類添加方法。函數(shù)調(diào)用表達式的抽象語法樹與非終結(jié)符primary對應(yīng)索抓。非終結(jié)符primary原本只表示字面量與變量名等最基本的表達式成分钧忽,現(xiàn)在,我們修改它的定義逼肯,使函數(shù)調(diào)用表達式也能被判斷為一種primary耸黑。即primary將涵蓋由primary后接括號括起的實參序列構(gòu)成的表達式,下圖是一個例子篮幢,它是由函數(shù)調(diào)用語句fact(9)構(gòu)成的抽象語法樹大刊。為了支持這一修改,我們需要為PrimaryExpr類添加若干新方法三椿。

圖 7.1 fact(9)的抽象語法樹

file

operand方法將返回非終結(jié)符primary原先表示的字面量與函數(shù)名等內(nèi)容缺菌,或返回函數(shù)名稱。postfix方法返回的是實參序列(若存在)赋续。eval方法將首先調(diào)用operand方法返回的對象的eval方法男翰。如果函數(shù)存在實參序列另患,eval方法將把他們作為參數(shù)纽乱,進一步調(diào)用postfix方法(在上圖中即Arguments對象)返回的對象的eval方法

PrimaryExpr類新增的postfix方法的返回值為Postfix類型。Postfix是一個抽象類(代碼清單7.9)昆箕,它的子類Arguments類是一個用于表示實參序列的具體類鸦列。ArgumentsEx修改器為Arguments類添加的eva1方法將實現(xiàn)函數(shù)的執(zhí)行功能

Arguments類新增的eval方法是函數(shù)調(diào)用功能的核心。它的第2個參數(shù)value是與函數(shù)名對應(yīng)的抽象語法樹的eval方法的調(diào)用結(jié)果鹏倘。希望調(diào)用的函數(shù)的Function對象將作為value參數(shù)傳遞給eval方法薯嗤。Function對象由def語句創(chuàng)建。函數(shù)名與變量名的處理方式相同纤泵,因此解釋器僅需調(diào)用eval方法就能從環(huán)境中獲取Function對象

之后骆姐,解釋器將以環(huán)境callerEnv為實參計算函數(shù)的執(zhí)行結(jié)果。首先捏题,F(xiàn)unction對象的parameters 方法將獲得形參序列玻褪,實參序列則由自身提供iterator方法獲取。然后解釋器將根據(jù)實參的排列順序依次調(diào)用eval并計算求值公荧,將計算結(jié)果與相應(yīng)的形參名成對添加至環(huán)境中带射。ParameterList類新增的eval方法將執(zhí)行實際的處理

實參的值將被添加到新創(chuàng)建的用于執(zhí)行函數(shù)調(diào)用的newEnv環(huán)境,而非callerEnv環(huán)境(表7.1)循狰。newEnv環(huán)境表示的作用域為函數(shù)內(nèi)部窟社。如果函數(shù)使用了局部變量券勺,它們將被添加到該環(huán)境

表7.1 函數(shù)調(diào)用過程中設(shè)計的環(huán)境

environment mean
newEnv 調(diào)用函數(shù)時新創(chuàng)建的環(huán)境。用于記錄函數(shù)的參數(shù)及函數(shù)內(nèi)部使用的局部變量
newEnv.outer newEnv的outer字段引用的環(huán)境灿里,能夠表示函數(shù)外層作用域关炼。該環(huán)境通常用于記錄全局變量
callerEnv 函數(shù)調(diào)用語句所處的環(huán)境。用于計算實參

最后匣吊,Arguments類的eval方法將在新創(chuàng)建的環(huán)境中執(zhí)行函數(shù)體盗扒。函數(shù)體可以通過調(diào)用Function對象的body方法獲得。函數(shù)體是def語句中由大括號{}括起的部分缀去,body方法將返回與之對應(yīng)的抽象語法樹侣灶。調(diào)用返回的對象的eva1方法即可執(zhí)行該函數(shù)

用于調(diào)用函數(shù)的環(huán)境newEnv將在函數(shù)被調(diào)用時創(chuàng)建,在函數(shù)執(zhí)行結(jié)束后舍棄缕碎。這與函數(shù)的參數(shù)及局部變量的生存周期相符褥影。若解釋器多次遞歸調(diào)用同一個函數(shù),它將在每次調(diào)用時創(chuàng)建新的環(huán)境咏雌。只有這樣才能正確執(zhí)行函數(shù)的遞歸調(diào)用

有時凡怎,用于計算實參的環(huán)境callerEnv與執(zhí)行def語句的是同一個環(huán)境,但也并非總是如此赊抖。callerEnv是用于計算調(diào)用了函數(shù)的表達式的環(huán)境统倒。如果在最外層代碼中調(diào)用函數(shù),callerEnv環(huán)境將同時用于保存全局變量氛雪。然而房匆,如果函數(shù)由其他函數(shù)調(diào)用,callerEnv環(huán)境則將保存調(diào)用該函數(shù)的外層函數(shù)的局部變量报亩。環(huán)境雖然支持嵌套結(jié)構(gòu)浴鸿,但該結(jié)構(gòu)僅反映了函數(shù)定義時的作用域嵌套情況。在函數(shù)調(diào)用其他函數(shù)時弦追,新創(chuàng)建的環(huán)境不會出現(xiàn)在這樣的嵌套結(jié)構(gòu)中

代碼清單7.7 FuncEvaluator.java

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);
        }
    }
}

代碼清單7.8 Function.java

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() + ">";
    }
}

代碼清單7.9 Postfix.java

package stone.ast;
import java.util.List;

public class Postfix extends ASTList {

    public Postfix(List<ASTree> list) {
        super(list);
    }
}

7.4 計算斐波那契數(shù)

至此岳链,Stone語言已支持函數(shù)調(diào)用功能。
代碼清單7.10是解釋器的程序代碼劲件,
代碼清單7.11是解釋器的啟動程序掸哑。
解釋器所處的環(huán)境并不是一個BasicEnv對象,而是一個由啟動程序創(chuàng)建的NestedEnv對象

下面我們以計算斐波那契數(shù)為例測試一下函數(shù)調(diào)用功能零远。代碼清單7.12是由Stone語言寫成的斐波那契數(shù)計算程序苗分。程序執(zhí)行過程中,將首先定義fib函數(shù)遍烦,并計算fib(10)的值俭嘁。最后輸出如下結(jié)果

=> fib
=> 55

代碼清單7.10 FuncInterpreter.java

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);
            }
        }
    }
}

代碼清單7.11 FunRunner.java

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);
    }
}

代碼清單7.12 計算斐波那契數(shù)列的Stone語言程序

def fib(n) {
    if n < 2 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}
fib(10)

7.5 為閉包提供支持

簡單來講,閉包是一種特殊的函數(shù)服猪,它能被賦值給一個變量供填,作為參數(shù)傳遞至其他函數(shù)拐云。閉包既能在最外層代碼中定義,也能在其他函數(shù)中定義近她。通常叉瘩,閉包沒有名稱

如果Stone語言支持閉包,下面的程序?qū)⒛苷_運行

inc = fun (x) { x + 1 }
inc(3)

這段代碼將創(chuàng)建一個新的函數(shù)粘捎,它的作用是返回一個比接收的參數(shù)大1的值薇缅。該參數(shù)將被賦值給變量inc。賦值給變量的就是一個閉包攒磨。inc并非函數(shù)的名稱泳桦,事實上,這種函數(shù)沒有名稱娩缰。不過灸撰,程序能夠通過inc(3)的形式,以3為參數(shù)調(diào)用該函數(shù)拼坎。讀者可以將其理解為浮毯,程序從名為inc的變量中獲得了一個閉包,并以3為參數(shù)調(diào)用這個閉包

代碼清單7.13是閉包的語法規(guī)則泰鸡。該規(guī)則修改了primary债蓝,向其中添加了閉包的定義

代碼清單7.13 閉包的語法規(guī)則

primary : " fun " param_list block | 原本的primary定義

7.6 實現(xiàn)閉包

代碼清單7.14是支持閉包功能的語法分析器程序。它修改了非終結(jié)符primary的定義盛龄,使語法分析器能夠解析由fun起始的閉包饰迹。代碼清單7.15的Fun類是用于表示閉包的抽象語法樹的節(jié)點類。

Fun類的eval方法通過代碼清單7.16的ClosureEvaluator修改器增加讯嫂。與def語句的eval方法一樣蹦锋,它也會創(chuàng)建一個Function對象。Function對象的構(gòu)造函數(shù)需要接收一個env參數(shù)欧芽,它是定義了該閉包的表達式所處的執(zhí)行環(huán)境。

def語句在創(chuàng)建Function對象后會向環(huán)境添加由該對象與函數(shù)名組成的鍵值對葛圃,而在創(chuàng)建閉包時千扔,eval方法將直接返回該對象。這樣一來库正,Stone語言就能將函數(shù)賦值給某個變量曲楚,或?qū)⑺鳛閰?shù)傳遞給另一個函數(shù),實現(xiàn)閉包的語法功能褥符。這時龙誊,實際賦值給變量或傳遞給函數(shù)的就是新創(chuàng)建的Function 對象。

代碼7.14 支持必爆的語法分析器ClosureParser.java

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));
    }
}

代碼清單7.15 Fun.java

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() + ")";
    }
}

代碼清單7.16 ClosureEvaluator.java

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);
        }
    }
}

代碼清單7.17 ClosureInterpreter.java

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());
    }
}

從具體實現(xiàn)的角度來看喷楣,整數(shù)值有Java語言的Integer對象表現(xiàn)趟大,字符串由String對象表現(xiàn)鹤树,而新添加的函數(shù)則由Function對象表現(xiàn)。

代碼清單7.17是支持閉包功能的Stone語言解釋器逊朽。代碼清單7.18是相應(yīng)的啟動程序

代碼清單7.18 ClosureRunner.java

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);
    }
}

雖然現(xiàn)在程序已經(jīng)支持函數(shù)何閉包了罕伯,Stone語言和其他很多變量無需聲明即可使用的語言一樣,但如果已經(jīng)存在某個全局變量叽讳,就是無法在創(chuàng)建同名變量追他,比方說下面的例子

x = 1
def foo (i) {
    x = i;
    x + 1
}

函數(shù)foo無法創(chuàng)建名為x的局部變量。函數(shù)中的x將引用第一行的全局變量x岛蚤。如果調(diào)用foo(3)邑狸,全局變量x的值就會是3,這可就麻煩了涤妒。想用的是局部變量推溃,實際使用的是全局變量,這里似乎存在大量錯誤隱患届腐。如果非要區(qū)分兩者铁坎,只要更改定義,讓全局變量的變量名必須以$開始就行了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犁苏,一起剝皮案震驚了整個濱河市硬萍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌围详,老刑警劉巖朴乖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異助赞,居然都是意外死亡买羞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門雹食,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畜普,“玉大人,你說我怎么就攤上這事群叶〕蕴簦” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵街立,是天一觀的道長舶衬。 經(jīng)常有香客問我,道長赎离,這世上最難降的妖魔是什么逛犹? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上虽画,老公的妹妹穿的比我還像新娘舞蔽。我一直安慰自己,他們只是感情好狸捕,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布喷鸽。 她就那樣靜靜地躺著,像睡著了一般灸拍。 火紅的嫁衣襯著肌膚如雪做祝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天鸡岗,我揣著相機與錄音混槐,去河邊找鬼。 笑死轩性,一個胖子當著我的面吹牛声登,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揣苏,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼悯嗓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卸察?” 一聲冷哼從身側(cè)響起脯厨,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坑质,沒想到半個月后合武,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡涡扼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年稼跳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃沪。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡汤善,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出巷波,到底是詐尸還是另有隱情萎津,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布抹镊,位于F島的核電站,受9級特大地震影響荤傲,放射性物質(zhì)發(fā)生泄漏垮耳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望终佛。 院中可真熱鬧俊嗽,春花似錦、人聲如沸铃彰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牙捉。三九已至竹揍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邪铲,已是汗流浹背芬位。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留带到,地道東北人昧碉。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像揽惹,于是被迫代替她去往敵國和親被饿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 等不及 秋風(fēng)的召喚 半黃了身軀 微紅了臉龐 爭先恐后 紛落至地上 回頭向果實 輕輕招手 來吧 胸懷已敞開 不止?jié)M滿溫暖
    葉一碧閱讀 242評論 0 10
  • 節(jié)日的儀式感搪搏,除了體現(xiàn)在吃和穿狭握,活動也是極其重要的部分。 九九重陽其實已過去好幾天了慕嚷,但這么好的載體哥牍,有關(guān)部門才不...
    其實不燙的蜂蜜麻花閱讀 193評論 0 1
  • 周末爬山回來剛進門就接到了好朋友劉建華的電話,劉建華是我在北京相處最輕松愉快的朋友喝检,他是在北京CBD上班的IT嗅辣,去...
    星寰Helen閱讀 846評論 13 5
  • Description There is a building of n floors. If an egg dr...
    6默默Welsh閱讀 376評論 0 0
  • 1.我十分幸運能夠擁有一個健康的身體,因為我是一個四肢健全的人挠说。 2.我感到十分快樂澡谭,并感激我的先生讓我擁有一個完...
    Sky_0b0c閱讀 108評論 0 0