Edited at

14 関数式に対応する

More than 1 year has passed since last update.


はじめに

以前の記事でScopeに対応しました。

今度は関数式に対応します。

前回の記事で対応したScopeもいかしてクロージャーを作ってみます。


関数式対応でやりたいこと

関数式対応でやりたいことを確認します。

例えば下のようなプログラムがあります。

外側の関数式が返す、内側の関数式を変数counterへ代入しています。

counterを関数として呼び出す度に、

外側の関数式で定義した変数cの値が加算されます。

最初のprintln(counter())1が出力され、

次のprintln(counter())2が出力されます。

counter = (function() {

var c = 0
return function() {
c = c + 1
return c
}
})()
println(counter())
println(counter())


実装の仕方

実装の仕方について、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。


構文解析(Parser)の実装の仕方

関数式の構文解析は、関数定義の構文解析と比べると、関数名があるかないかの違いです。

その違いを関数定義の構文解析に反映します。


インタプリタ(Interpreter)の実装の仕方

関数式を表すオブジェクトを導入します。

そのオブジェクトは値として扱えるようにし、かつ関数呼び出しにも対応します。

いままではIntegerのみが値として扱われるものでしたが、

関数式も値として扱えるようにしなければなりません。


Javaで実装してみる

実装にうつります。

構文解析(Parser)、インタプリタ(Interpreter)について、

変更と追加をしたところを順にみていきます。


Parser.java

Parser.javaの実装です。

関数定義を解析するfunc()メソッドの変更です。

変更箇所は最初のif文です。

元の実装ではfunctionトークンの後には、関数名にあたるidentトークンがくることを期待していました。

関数式の場合functionトークンの後は(トークンがきます。

if文の判定は、functionトークンの後が(トークンなら関数式の解析、

そうでなければ、関数定義の解析をするように変更しました。


Parser.java

    private Token func(Token token) throws Exception {

if (token().value.equals("(")) {
token.kind = "fexpr";
} else {
token.kind = "func";
token.ident = ident();
}
consume("(");
token.params = new ArrayList<Token>();
if (!token().value.equals(")")) {
token.params.add(ident());
while (!token().value.equals(")")) {
consume(",");
token.params.add(ident());
}
}
consume(")");
token.block = body();
return token;
}


Interpreter.java

Interpreter.javaの実装です。

変数を表すVariableクラスの変更です。

いままでは値はIntegerしか扱ってきませんでした。

関数式を導入するので、関数式も値として扱えなければなりません。

扱えるようにするためフィールド変数valueの型を、

ObjectIntegerから変更しました。


Interpreter.java

    public static class Variable {

public String name;
public Object value;

@Override
public String toString() {
return name + " " + value;
}
}


expression()メソッドの変更です。

式を表すトークンの意味(kind)によって分岐する処理です。

// Addの下へ関数式を表すトークンfexprのための分岐を追加しました。


Interpreter.java

    public Object expression(Token expr) throws Exception {

if (expr.kind.equals("digit")) {
return digit(expr);
} else if (expr.kind.equals("ident")) {
return ident(expr);
} else if (expr.kind.equals("func")) {
return func(expr);
// Add
} else if (expr.kind.equals("fexpr")) {
return fexpr(expr);
} else if (expr.kind.equals("paren")) {
return invoke(expr);
} else if (expr.kind.equals("sign") && expr.value.equals("=")) {
return assign(expr);
} else if (expr.kind.equals("unary")) {
return unaryCalc(expr);
} else if (expr.kind.equals("sign")) {
return calc(expr);
} else {
throw new Exception("Expression error");
}
}

上のexpressionメソッドで追加された分岐で呼び出される、fexpr()メソッドを追加しました。

関数式を表すオブジェクトを生成します。

関数式がクロージャーで使われることを考えて、関数式の親スコープを保持できるようにするため、

func.contextには現状のInterpreterをクローンして保持します。


Interpreter.java

    public Object fexpr(Token token) throws Exception {

List<String> paramCheckList = new ArrayList<String>();
for (Token p : token.params) {
String param = p.value;
if (paramCheckList.contains(param)) {
throw new Exception("Parameter name was used");
}
paramCheckList.add(param);
}
DynamicFunc func = new DynamicFunc();
func.context = new Interpreter();
func.context.global = global;
func.context.local = local;
func.context.body = body;
func.params = token.params;
func.block = token.block;
return func;
}

関数定義を表すインスタンスを生成するfunc()メソッドの変更です。

fexpr()メソッドが現状のInterpreterをクローンするのに同調して、

こちらも// Updateの下でクローンするように変更しました。


Interpreter.java

    public Object func(Token token) throws Exception {

String name = token.ident.value;
if (local.functions.containsKey(name)) {
throw new Exception("Name was used");
}
if (local.variables.containsKey(name)) {
throw new Exception("Name was used");
}
List<String> paramCheckList = new ArrayList<String>();
for (Token p : token.params) {
String param = p.value;
if (paramCheckList.contains(param)) {
throw new Exception("Parameter name was used");
}
paramCheckList.add(param);
}
DynamicFunc func = new DynamicFunc();
// Update
func.context = new Interpreter();
func.context.global = global;
func.context.local = local;
func.context.body = body;
func.name = name;
func.params = token.params;
func.block = token.block;
local.functions.put(name, func);
return null;
}

value()メソッドの変更と、integer()メソッドの追加です。

value()メソッドは、引数が1のような値そのものだったらその値を返し、

変数だったら、変数が保持する値を返します。

今までは値にIntegerしか扱ってきませんでした。

対応する関数式も、このメソッドで返す値になるので対応します。

value()メソッドの返り値の型を、IntegerからObjectへ変更します。

これでIntegerも関数式もvalue()メソッドで返せます。

integer()メソッドを追加した理由は、

value()メソッドの返り値をIntegerからObjectへ変更したためです。

今まではvalue()メソッドの返り値がIntegerで保証されていましたが、

IntegerからObjectへ変更によって保証されなくなりました。

代わりにinteger()メソッドで値がIntegerであることを保証します。

これらの変更に影響して、値がIntegerであることを保証するために、

value()メソッドの呼び出していた、

unaryCalc()メソッドやcalc()メソッドでも、

代わりにinteger()メソッドを呼び出すように変更しています。


Interpreter.java

    public Object value(Object value) throws Exception {

if (value instanceof Integer) {
return value;
} else if (value instanceof Func) {
return value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return value(v.value);
}
throw new Exception("right value error");
}

public Integer integer(Object value) throws Exception {
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return integer(v.value);
}
throw new Exception("right value error");
}


func()メソッドの変更です。

引数が関数であることを保証するメソッドです。

else ifのところへ、引数がVariable型だった場合、

その値が関数であることを保証する処理を追加しました。


Interpreter.java

    public Func func(Object value) throws Exception {

if (value instanceof Func) {
return (Func) value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return func(v.value);
} else {
throw new Exception("Not a function");
}
}

isTrueメソッドの変更です。

isTrueメソッドは値を真偽値へ解決する処理です。

いままでは値はIntegerだけでしたが、関数も値として扱うようになったので、

値が関数だったときは、単純にtrueへ解決するようにしました。


Interpreter.java

    public boolean isTrue(Object value) throws Exception {

if (value instanceof Integer) {
return 0 != ((Integer) value);
} else if (value instanceof Func) {
return true;
} else {
return false;
}
}

以上の実装を使って下のプログラム

counter = (function() {

var c = 0
return function() {
c = c + 1
return c
}
})()
println(counter())
println(counter())

を実行し、

最初のprintln(counter())1が出力され、

次のprintln(counter())2が出力されます。


Interpreter.java

    public static void main(String[] args) throws Exception {

String text = "";
text += "counter = (function() {";
text += " var c = 0";
text += " return function() {";
text += " c = c + 1";
text += " return c";
text += " }";
text += "})()";
text += "println(counter())";
text += "println(counter())";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> 1
// --> 2
}

実装は以上です。

ありがとうございました。


おわりに

ソースの全文はこちらで公開しています。

Calc

https://github.com/quwahara/Calc/tree/article-14-function-expression-r3/Calc/src/main/java

続きの記事があります。

文字列に対応する

http://qiita.com/quwahara/items/ddcbc8c37b9d442fe0f2