kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Java 7のクロージャ(BGGA版)のプロトタイプを試してみた(2) - control invocation syntax

参考URLはこの辺:http://tronicek.blogspot.com/2008/02/version-2008-02-22.html

BGGA版のクロージャには、シンタックスシュガーとして、control invocation syntaxというものがあり、クロージャ(というか無名関数。いい加減しつこいけど)を最後の引数に取るようなメソッドの呼び出しを簡潔に書くための記法を提供してくれる。

control invocation syntaxは次のような感じで書くことができ、

withIndex(String s, int i:args) {
  System.out.printf("%s, %d%n", s, i);
}

これは以下の呼び出しをしたのと同じ意味になる(らしい)。

withIndex(args, {String s, int i ==> System.out.printf("%s, %d%n", s, i); });

無名関数のところで、=>ではなく、==>という見慣れ無い記法が出てくるが、これは、unrestricted closureというもので、unrestricted closureの中では、break, return,continueを使用することができる。unrestricted closureを引数に取るメソッドを使うことで、(forやwhileのような)一級の制御構文に近い使い勝手を持つ、新しい制御構文を定義できるようになる。

というわけで、早速control invocation syntaxを使えるようなメソッドを定義してみることにする。まずは、上記のwithIndexメソッド。第1引数が配列で、第2引数が配列の要素とintを受け取ってvoidを返すunrestricted closureなので、シグネチャ

<T> void withIndex(T[] arr, {T, int ==> void} block)

のようになる。また、withIndexの内部では任意の例外が投げられる可能性があるので、それに対応できるようにするため、型パラメータとしてthrows Eを宣言しておくことにする。

<T, throws E> void withIndex(T[] arr, {T, int ==> void throws E} block) throws E

withIndexの本体では、配列をループで回しながらループ回数をカウントして、各ループごとに配列の要素とループ回数を引数として、blockを呼び出すので、こんな感じになる。

public static <T> void withIndex(T[] arr, {T, int ==> void} block) {
  int count = 0;
  for(T t:arr) {
    block.invoke(t, count);
    count++;
  }
}

一度このようなメソッドを定義してしまえば、このメソッドの利用者は、
無名関数を引数に取るメソッドではなく、単に

withIndex(T[] var1, int var1: T[]型の式) 文

というwithIndex構文が定義されたと思って使うことができる。

withIndexの場合、たとえば次のように使うことができる。

public static void main(String[] args){
  withIndex(String arg, int i:args) {
    System.out.printf("%s, %d%n", arg, i);
  }
} 

これは、argsから各要素を取り出し、要素の値と要素の添え字を","で
区切って出力するという単純なプログラムだ。Javaに慣れた人であれば、
拡張for文のようなシンタックスシュガーが追加されただけと思えば
良いのではないだろうか。

動作するプログラムの全体は以下のようになる。

import java.util.*;

public class ControlInvocationTest {
  public static <T, throws E> void withIndex(
    T[] arr, {T, int ==> void throws E} block
  ) throws E {
    int count = 0;
    for(T t:arr) {
      block.invoke(t, count);
      count++;
    }
  }
  public static void main(String[] args){
    withIndex(String arg, int i:args) {
      System.out.printf("%s, %d%n", arg, i);
    }
  } 
}

実行してみると次のような出力が得られ(コンパイルは省略)、思った
通りの動作を行っていることがわかる。

>java ControlInvocationTest A B C
A, 0
B, 1
C, 2

さて、せっかくcontrol invocation syntaxを使うのだから、breakやcontinueも試してみようというわけで、mainメソッドの本体を以下のように書き換えて試して見たのだが…

withIndex(String arg, int i:args) {
  if(i == 1) break;
    System.out.printf("%s, %d%n", arg, i);
  }
}

結果はコンパイルエラー。

>bin\javac ControlInvocationTest.java
ControlInvocationTest.java:15: break が switch 文またはループの外にあります。
      if(i == 1) break;

「breakがswitch文またはループの外にあります」って、話が違うじゃねえかよおと思いつつ、念のためcontinueやreturnの場合も試してみた。

continueの場合:

withIndex(String arg, int i:args) {
  if(i == 1) continue;
  System.out.printf("%s, %d%n", arg, i);
}
>bin\javac ControlInvocationTest.java
ControlInvocationTest.java:15: continue がループの外にあります。
      if(i == 1) continue;

やはり結果は同じ。

returnの場合:

withIndex(String arg, int i:args) {
  if(i == 1) return;
  System.out.printf("%s, %d%n", arg, i);
}
>bin\javac ControlInvocationTest.java
ControlInvocationTest.java:15: 'return' out of a closure not implemented
      if(i == 1) return;

…単に未実装だったというオチらしい。念のため、breakやcontinueの外側にループを配置した場合も試してみた。

while(true) {
  withIndex(String arg, int i:args) {
    if(i == 1) break;
    System.out.printf("%s, %d%n", arg, i);
  }
  break;
}
>bin\javac ControlInvocationTest.java
ControlInvocationTest.java:16: 'break' out of a closure not implemented
        if(i == 1) break;
>bin\javac ControlInvocationTest.java
while(true) {
  withIndex(String arg, int i:args) {
    if(i == 1) continue;
    System.out.printf("%s, %d%n", arg, i);
  }
  break;
}
ControlInvocationTest.java:16: 'continue' out of a closure not implemented
        if(i == 1) continue;

うーむ。やっぱり単に未実装だったってオチか。よく考えてみれば、参考URLの本文では、

4. Unrestricted closures were partially implemented.

と書かれているわけで、それくらい予想してしかるべきだったかも。

続いて、こないだ作った、ブロックの実行が終わったら必ず閉じるopenのようなものをcontrol invocation syntax対応にしてみた。とは言っても、変更点は、=>を==>にするだけなので、非常に簡単。

public static <T, throws E> T open(
  String filePath, {Reader ==> T throws E} block
) throws FileNotFoundException, E {
  FileReader reader = new FileReader(filePath);
  try {
    return block.invoke(reader);
  } finally {
    try { 
      out.printf("closing %s...%n", filePath);
      reader.close(); 
    } catch (IOException ex) { 
      ex.printStackTrace();
    }
  }
}

openを利用する側は、こんな感じ。

open(Reader r:"input.txt") {
  out.println("read started");
  StringBuilder buf = new StringBuilder();
  for(int ch; (ch = r.read()) != -1;) buf.append((char)ch); 
  out.println("read finished");
  System.out.println(new String(buf));
}

一つ残念なのは、control invocation syntaxは文であり、どうやら値を返すことはできないようなので、openに渡したブロックの最後の式の評価結果をopenの呼び出し側に返すと言ったことはできないことだが、control invocation syntaxを式としても解釈できるようにしてしまうと文法がますますややこしくなりそうではあるし、仕方無いかもしれない。

追記:
未実装の話はともかくとして、unrestricted closure内でのbreak/continueのセマンティクスをなんか勘違いしてる気がしてきた。Rubyでブロック内でbreak/nextした場合と似たようなものかと思ってたんだけど、ひょっとして、一番内側のwhile/do while/for/拡張for文に対してbreak/continueするんだろうか?後で仕様をちゃんと読んでみよう。

追記2:
break/continueについては、次のように、メソッドの宣言と呼び出し側両方にforを付けると意図通り(control invocation syntaxを言語組み込みのループ構文と同様にとらえた場合の動作)に動作するようです(コメント欄参照)。ただし、現在のプロトタイプ実装では、breakの方は未実装っぽいですが。

public static for <T, throws E> void withIndex(
  T[] arr, {T, int ==> void throws E} block
) throws E {
  int count = 0;
  for(T t:arr) {
    block.invoke(t, count);
    count++;
  }
}
for withIndex(String arg, int i:args) {
  if(i == 1) continue;
  System.out.printf("%s, %d%n", arg, i);
}