はじめに
以前の記事で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トークンの後が(トークンなら関数式の解析、
そうでなければ、関数定義の解析をするように変更しました。
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の型を、
ObjectへIntegerから変更しました。
public static class Variable {
public String name;
public Object value;
@Override
public String toString() {
return name + " " + value;
}
}
expression()メソッドの変更です。
式を表すトークンの意味(kind)によって分岐する処理です。
// Addの下へ関数式を表すトークンfexprのための分岐を追加しました。
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をクローンして保持します。
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の下でクローンするように変更しました。
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()メソッドを呼び出すように変更しています。
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型だった場合、
その値が関数であることを保証する処理を追加しました。
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へ解決するようにしました。
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が出力されます。
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