はじめに
以前の記事で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