式指向構文が言語処理系にもたらす複雑性

式の中に分岐や反復、ジャンプを書ける構文を 式指向の構文 と呼ぶことにする。 式指向の構文は便利な反面、言語に一定の複雑性を追加する。

式指向の構文の例

例えば Rust は式指向の構文を持ち、条件分岐を表す if はそれ自体が式になっている。分岐の結果として評価された節の値が if 式の値になる。

let x = if p() {
    f()
} else {
    g()
};

以降、サンプルコードは Rust 風の構文で書く。ただし型システムや処理系は Rust とは限らない。

ジャンプとスタックマシンの相性の悪さ

式指向の構文を持つ場合、式の評価の途中でジャンプすることができる。

fn f() -> i32 {
    1 + { return 2; }
}

この関数をスタックマシンのコードにナイーブにコンパイルすると、次のような不正な振る舞いをする。

  • 1 をスタックに積む
  • 2 をスタックに積む
  • return する。スタックから返り値 2 を pop して、関数から抜ける
  • スタックから2つの値を pop して足し、和をスタックに積む (← ジャンプしたのでこれは実行されない)

ここで 1 はスタックに置かれたままであり、関数の実行の前後でスタックの深さが維持されない。

解決策

この問題を解決するには、レジスタマシンを使うか、ジャンプする際に不要になった値をスタックから pop するようなコードを生成すればいい。

(追記: 正準化(canonicalize)などの手順により、式の評価の途中でジャンプが起きないように変形しておく方法もある。近況 2019-04-30 に似たような話を書いた。)

ジャンプする式の型

静的型システムを持つ式指向言語の場合、break/return などのジャンプ式にも静的な型をつける必要がある。

例えば次の関数に含まれる match 式は Some のアームが式の値を持ち、None のアームは return する。 mach 式の値は i32 でなければいけないので、return 式の型は i32 か、それの部分型でなければいけない。

fn drain_sum(xs: &mut Vec<i32>) -> i32 {
    let mut total = 0;
    loop {
        let x = match xs.pop() {
            Some(it) => it,
            None => return total,
        };

        total += x;
    }
}

Rust の return 式には ! (never) 型がつく。! はあらゆる型の部分型なので、i32 の部分型でもある。 したがって match 式に i32 型をつけられる。

もう1つの方法として、return などのジャンプする式の出現ごとにフレッシュなメタ型 T を生成して (return expr): T とする方法がある。 単一化により型 T が他のアームの型になるので、型検査を通せる。 この方法だと部分型の仕組みを導入しなくて済む。 (こういった理由から ML の failwith 関数に failwith<'a> : string -> 'a という型がついている。)

いずれにせよ「never 型と部分型」や「メタ型変数と単一化」のような一定の仕組みの導入が要請される。

あるいは、妥協して、ジャンプする式には決め打ちで unit (あるいは (), void, null の) 型をつけるという案もある。 (この手法を採用している言語があるかは知らない。) その場合、上記のような match 式は型検査を通らなくなり、式指向らしさの一部が失われる。

(追記: C++ のパターンマッチ機能である inspect 式の提案では、制御を返さないブランチに印をつけておくことで、そのブランチの評価値の型を型検査時に無視するような機能がある。 参考: Pattern Matching - p1371r3.pdf)

if 式の後ろのセミコロン

C言語風の構文で、セミコロンが必須の言語を考える。

単純に if 文の構文規則を「式」に移動すると、従来の if 文と同様の用途で if 式を使うとき、末尾に ; が必要になってしまう。

    if (cond) {
        body
    };
//   ^ 式の終わりなのでセミコロンが必要

if (cond) { ... } は1個の式であって文ではないので、文として書きたいなら式文にする必要があり、末尾にセミコロンをつけることになる。この問題を解決するため、一定の考慮が必要になる。

解決策

文の先頭に if 式が来るときは、それだけで1個の式文とみなし、後ろにセミコロンがなくてもよいことにすればよい。(部分的なセミコロン省略ルール。)

ただしこれだけでは if flip() { you } else { opponent }.win(); のような if 式から始まる式文を正しくパースできないことになる。これは JavaScript において、即時実行関数を function(){}() と書くと function 宣言とみなされてしまう状況に似ている。回避策として (function(){})() などと書くのは受け入れられているので、そのようにしてもよいだろう。

また、Rust では 打ち切り規則 と呼ばれる構文上の規則を用意している。打ち切り規則は、おおまかにいうと「文頭に if 式があるなら、それが明らかに他の式の一部でなければ末尾のセミコロンの省略を許す」というもの。例えば次のコードは2つの文にパースされる:

    if cond { ... }  //< 末尾にセミコロンをつけていないが、
    f();             //  f から次の文が始まる。

一方、次のコードでは if 式の直後に . があって、明らかに if 式はその左辺となる。このケースではセミコロンの省略は起きない。 (特別扱いされているのは .? だけで、二項演算子は打ち切られる模様: if cond { 42 } / 2 など)

    if cond { ... }.f();
    //             ^ . は左辺に式が必要なので文のパースを続ける

参考: Rustの文でセミコロンを省略してよい条件 - 簡潔なQ

関連記事