1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

14 関数式に対応する

Last updated at Posted at 2017-04-09

はじめに

以前の記事で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

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?