LoginSignup
1
1

More than 5 years have passed since last update.

9 戻り値に対応する

Last updated at Posted at 2017-03-20

はじめに

以前の記事で複数の引数に対応しました。
続けて関数の戻り値に対応したいと思います。

関数の戻り値対応でやりたいこと

簡単にやりたいことを確認します。
例えば下のようなプログラムがあったら、add3()関数呼び出しの戻り値が返されて変数vに代入されます。
標準出力には変数vの値6が出力されることを目指します。

function add3(a1, a2, a3) {
  return a1 + a2 + a3
}
v = add3(1,2,3)
println(v)

変数のスコープに対応しないのは、以前の記事のままです。

実装の仕方

実装の仕方について、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。
字句解析(Lexer)はとくに変更しません。

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

returnの構文解析を実装するにあたり、returnの構文に注意する必要があります。
returnの構文はreturn 1のように、戻り値が指定される場合もあれば、
returnだけ指定され戻り値が指定されない場合もあります。
returnだけの指定が構文として正しいのなら、
return 1と書かれた場合に、
それは戻り値を指定された場合の構文ともとれるし、
1returnとは独立した構文であると考えて、
戻り値が指定されない場合の構文ともとれます。
このどっちともとれる状況を簡単に解決するために、手抜きな方法をとります。
その方法はreturnトークンのあとに}トークンがきたら、
戻り値が指定されない場合のreturnとみなします。
逆に}トークンがこないなら、戻り値が指定される場合のreturnとみなします。
戻り値が指定されない場合の解析は変数名と同じように扱い、
戻り値が指定される場合の解析は、- 1のような単項演算子と同じように扱います。

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

インタプリタでは式を逐次実行するメソッドbody()へ変更を加えます。
body()の中でreturnトークンにであったら、
逐次実行を中断して呼び出し元へ戻るようにします。
またreturnは関数内でしか呼び出しが許されません。
関数内でのreturn呼び出しかの判定も行います。

Javaで実装してみる

実装にうつります。
構文解析(Parser)、インタプリタ(Interpreter)について、
変更と追加をしたところを順にみていきます。

Parser.java

Parser.javaの実装です。
トークンの意味に対して、どう動作するかの定義を追加します。
returnは予約語となるので、
<-- Updateとあるところにreturnを追加しました。

Parser.java
    public Parser() {
        degrees = new HashMap<>();
        degrees.put("(", 80);
        degrees.put("*", 60);
        degrees.put("/", 60);
        degrees.put("+", 50);
        degrees.put("-", 50);
        degrees.put("=", 10);
        factorKinds = Arrays.asList(new String[] { "digit", "ident" });
        binaryKinds = Arrays.asList(new String[] { "sign" });
        rightAssocs = Arrays.asList(new String[] { "=" });
        unaryOperators = Arrays.asList(new String[] { "+", "-" });
        reserved = Arrays.asList(new String[] { "function", "return" });    // <-- Update
    }

解析を行う部分の変更です。
returnの解析を行うif文を<-- Addがある箇所へ加えました。
returnトークンの次のトークンが、閉じの波カッコの種類を表すeobでなければ、
値を返すreturnと判断し、戻り値になるトークンをleftフィールドへ保持します。

Parser.java
    private Token lead(Token token) throws Exception {
        if (token.kind.equals("ident") && token.value.equals("function")) {
            return func(token);
        } else if (token.kind.equals("ident") && token.value.equals("return")) {    // <-- Add
            token.kind = "ret";
            if (!token().kind.equals("eob")) {
                token.left = expression(0);
            }
            return token;
        } else if (factorKinds.contains(token.kind)) {
            return token;
        } else if (unaryOperators.contains(token.value)) {
            token.kind = "unary";
            token.left = expression(70);
            return token;
        } else if (token.kind.equals("paren") && token.value.equals("(")) {
            Token expr = expression(0);
            consume(")");
            return expr;
        } else {
            throw new Exception("The token cannot place there.");
        }
    }

Interpreter.java

Interpreter.javaの実装です。

式を逐次実行するメソッドbody()への変更です。

body()は戻り値を返さないvoid型でしたが、戻り値を返せるようにObject型に変更しました。
body()のシグニチャーにboolean[] retを加えました。
retの役割りは2つです。
1つめはretnullでないなら、returnが可能な状況であることを表します。
2つめはreturnにであったら、呼び出し元にもreturnにであったことを伝播します。
retbooleanの配列型にしたのは、呼び出し元に値を返すためです。
本来の配列を使う目的ではない、邪道な使い方をしています。
C#のrefキーワードをつけた引数のような働きを、簡単にさせたくてそうしました。

forの中で最初に、逐次実行するトークンがreturnかを判定します。
returnでないなら、いままでのbody()と同じ処理です。
returnならreturn可能な状況か判定します。
retreturnにであったことを呼び出し元に伝えるためにtrueを代入します。
戻り値があるreturnの場合は、戻り値あたるトークンをexpression()で実行します。

Interpreter.java
    public Object body(List<Token> body, boolean[] ret) throws Exception {
        for (Token exprs : body) {
            if (exprs.kind.equals("ret")) {
                if (ret == null) {
                    throw new Exception("Can not return");
                }
                ret[0] = true;
                if (exprs.left == null) {
                    return null;
                } else {
                    return expression(exprs.left);
                }
            } else {
                expression(exprs);
            }
        }
        return null;
    }

DynamicFuncクラスのinvoke()メソッドの変更です。
<-- Updateが変更箇所です。
body()メソッドのシグニチャーと戻り値の変更に対応しています。
引数retにインスタンスを代入して、returnできる状況であることを表しています。
body()の戻り値をそのままinvoke()の戻り値にします。

Interpreter.java
    public static class DynamicFunc extends Func {

        public Interpreter context;
        public List<Token> params;
        public List<Token> block;

        @Override
        public Object invoke(List<Object> args) throws Exception {
            for (int i = 0; i < params.size(); ++i) {
                Token param = params.get(i);
                Variable v = context.variable(context.ident(param));
                if (i < args.size()) {
                    v.value = context.value(args.get(i));
                } else {
                    v.value = null;
                }
            }
            boolean[] ret = new boolean[1]; // <-- Update
            return context.body(block, ret); // <-- Update
        }
    }

run()メソッドの変更です。
<-- Updateが変更箇所です。
body()メソッドのシグニチャーと戻り値の変更に対応しています。
run()が呼び出されている状況は、関数の中ではありません。
その状況はreturnできない状況なので仮引数retnullを指定します。

Interpreter.java
    public Map<String, Variable> run() throws Exception {
        body(body, null); // <-- Update
        return variables;
    }

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

function add3(a1, a2, a3) {
  return a1 + a2 + a3
}
v = add3(1,2,3)
println(v)

を実行し、変数vへ代入される値6を標準出力へプリントします。

Interpreter.java
    public static void main(String[] args) throws Exception {
        String text = "";
        text += "function add3(a1, a2, a3) {";
        text += "  return a1 + a2 + a3";
        text += "}";
        text += "v = add3(1,2,3)";
        text += "println(v)";
        List<Token> tokens = new Lexer().init(text).tokenize();
        List<Token> blk = new Parser().init(tokens).block();
        new Interpreter().init(blk).run();
        // --> 6
    }

実装は以上です。
ありがとうございました。

おわりに

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

Calc
https://github.com/quwahara/Calc/tree/article-9-return-r2/Calc/src/main/java

続きの記事があります。

if文に対応する
http://qiita.com/quwahara/items/96a68cdee4f2a0452836

1
1
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
1