2011/12/01

Project Lambda

このエントリーをはてなブックマークに追加

このエントリは Java Advent Calendar 2011 の一環で、第 1 日目のエントリーです。

やっぱり、櫻庭といえば Java SE の新しいところでしょう (これ去年も書いたなぁ ^ ^;;)。ということで、Java SE 8 の目玉機能となっている Project Lambda を取りあげます。

結果的に超長文になってしまいましたww

 

厳密にいえば、Lambda はクロージャではありません。

追記: ここでいっているクロージャはレキシカルスコープが使用できる無名関数のことを意味しています。 (るとさん、コメントありがとうございました)

 

では、ラムダ式とはなんなのか?

それはタスクを簡単に書くことを目的とした、単一メソッドのインタフェースのインスタンス生成を簡単に書くための簡易記法です。ようするにシンタックスシュガーでしかありません。

ここでいうタスクは、特にパラレルに処理するタスクのことです。

今年の 7 月から 9 月まで Java SE 7 のラウンチイベントで、いろいろなところで Fork/Join Framework の講演をさせていただいたのですが、この Fork/Join Framework は Project Lambda と深い関係をもっています。

というのも、Project Lambda ができたのは Fork/Join Framework 抜きでは語れないからです。つまり、Fork/Join Framework で処理するタスクを Project Lambda を使って簡単に書きましょうというのが、本来の目的であるからです。

ここらへんのことは、ITpro の Java 技術最前線などで何度か書いてきたのですが、もう一度まとめてみましょう。

Project Lambda 小史

櫻庭が知りうる限り、Java へのクロージャの導入について公に発言されたのは、2005 年の JavaOne の Technical General Session です。

この当時、Technical General Session で Java SE を担当していたのが Graham Hamilton です。

この時は、まだ Java SE 6 もリリースされていない頃です。じゃあ、なんで Java SE 7 の話をしたかというと、もともと Java SE 6 と Java SE 7 は一緒に仕様が検討されて、導入が簡単な機能を Java SE 6、大変なものを Java SE 7 にするということだったのです。

で、Graham Hamilton が Java SE 7 に導入するといって列挙したキーワードはクロージャ、friend (その後、Project Jigsaw に続きます)、XML リテラル (完全に抹殺されてしまいました) などです。

これを受けて、クロージャの仕様を提案したのが、Neal Gafter を中心にした Gilad Bracha、James Gosling、Peter van der Ahé のグループ。これが 2006 年のことです。

彼らの提案は、彼らの頭文字を取って BGGA と呼ばれていました。まだ、その提案は彼らのサイトで見ることができます。

この提案はレキシカルスコープを含んだ、本当の意味でのクロージャです。

これが大議論に発展してしまったのです。いわゆる炎上です。クロージャ導入に賛成のグループと反対のグループが真っ向から対抗してしまいました。

賛成派の急先鋒は、BGGA の Neal Gafter。これに対して反対派の急先鋒は Joshua Bloch。

この 2 人は知っている人は知っている、Java Puzzlers のコンビで大の仲良し。ところが、このクロージャ論議で仲違いしてしまったほど、議論が伯仲してしまったのです。

Joshua Bloch と Doug Lea、Bob Lee は BGGA に対抗してクロージャを使わない CICE を提案しています。後から考えると、CICE が提案してる内容がいちばん Project Lambda に近いですね。

また、これとは別に Stephen Colebourne らが FCM を提案しています。

このようにクロージャの提案に関して群雄割拠の状況になってしまって、まったく収集がつかなくなってしまいました。

そして、2008 年の 11 月の Devoxx 2008 において、Mark Reinhold がクロージャは見送る宣言をしてしまいました。ようするに誰もこの議論をまとめられなくなってしまったのだと思います。

ところが、ところが、次の年の Devoxx において、再び Mark Reinhold が Project Lambda を立ち上げると宣言したのです!!

なぜ、1 年で状況が変わってしまったのか。その理由がマルチコアの CPU の台頭なのです。

たくさんのコアがある状況で、性能をあげるには、すべてのコアを遊ばせないようにしなくてはなりません。Java で複数のコアを動作させるのに使うのは Thread です。でも、Thread をそのまま使うというのはありえません。通常は Concurrency Utilities で提供されているスレッドプールを使います。

ところが、Concurrency Utilities で想定されているタスクは粒度が大きいのです。たとえば、トランザクションの単位でタスクを割り振るとかですね。

粒度が大きいタスクで問題になるのは、同期が発生した場合や、競合が発生した場合です。たとえば、とあるオブジェクトをロックすると他のスレッドからは使えません。ロックが外れるまで他のスレッドは待たなくてはいけません。つまり、他のスレッドはロックが外れるまで遊んでしまうわけです。

なので、粒度の小さいタスクにして、同期のコストがなるべく小さくなるようにする必要があります。

だったら、Concurrency Utilities で粒度の小さいタスクを書けばいいと思いますが、残念ながらそれだとオーバーヘッドが非常に大きくなってしまうのです。

つまり、粒度の小さいタスクを扱うには、それ専用のフレームワークが必要になるわけです。それが JSR 166y の Fork/Join Framework です。

これで粒度の小さいタスクを書く準備はできました。バリバリとタスクを書いていきましょう。.... と思ったのですが、なかなかことはそううまく運びません。

Java でタスクを書くとしたら、インタフェースを定義して、無名クラスで記述するのが一番多いやり方だと思います。たとえば、イベント処理なんかがそうですね。こういうやつです。

        JButton button = new JButton("OK");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // イベント処理
                update();
            }
        });

でも、よく考えてみると実際に書きたい部分は update メソッドをコールする部分だけです。

なのに、無名クラスの定義やら、メソッドのオーバーライドやら無駄なものがいっぱい。

しかも、汎用のタスクを書けるようにしたら、シグネチャが異なるメソッドをいっぱいオーバーロードしなくてはいけません。たとえば、JSR 166y のおまけともいうべき extra166y で提供している ParallelArray クラスは、内部イテレーターでタスクをパラレルに処理できます (これについては後でまた書きます)。

ParallelArray クラスで処理するタスクは Ops クラスで定義しているインタフェースで書くのですが、int やら double やらの引数を変えたシグネチャの op メソッドを用意するため、なんと 100 種類以上もあります。

これらのインタフェースの種類を覚えるのも難儀ですよね。

で、もっとタスクを簡単に書けるようにしましょうというのが、Project Lambda の発端なわけです。

BGGA などそれまでの提案は、Java にクロージャを導入するということが目的になっていました。なので、議論がまとまらなくなってしまったわけです。

それに対して、Project Lambda の目的はクロージャを導入することではないのです。あくまでも、タスクを簡単に書けるようにしましょうというのが目的です。

その目的のために必要なことを検討していくということにしたために、やっと議論がまとまったわけです。

 

しかし、Project Lambda もあっさりと仕様が決まったわけではないです (実際、まだ最終決定はされていないです)。

Project Lambda の発足時に Mark Reinhold が Straw-Man Proposal という提案を行っています (Straw-Man というのはかかしのことですけど、たたき台的な意味で使われるようです)。

Straw-Man Proposal では

  • 関数形の導入
  • レキシカルスコープは検討していく (アノテーションを使って指定する)

ことを提案しています。

しかし、この後、レキシカルスコープはすぐに却下されてしまいました。そして、関数形も却下し、無名クラスのインスタンス生成を簡単に書く記法に落ち着いたのです。

記法も二転三転しています。

ここらへんの議論に関しては DeveloperWorkds での Brian Goetz のアーティクルが参考になります。

ということで、Project Lambda はクロージャではなくなったのです。

しかし、Project Lambda の目的を考えると、現在の仕様は妥当であると櫻庭は思っています。

ちなみに、Joshua Bloch と Neal Gafter ですが、Project Lambda の立ち上げ後、仲直りしたらしいです。とはいっても、その後 Java Puzzlers で 2 人の "Click and Hack, the Type-It Brothers" は復活していないのですが...

Project Lambda のインストール

Java SE 8 は OpenJDK において、毎週ビルドが公開されています。しかし、現状のビルド (2011/12/1 現在で build 14) では Project Lambda のソースはまだ取り込まれていません。

とはいうものの、Project Lambda がまったく実装されていないというわけではなく、ちゃんと実装が進んでいます。

最近になって、Lambda の評価用に JDK 7 に Project Lambda を組み込んだパッケージが公開されました。今回はこれを使って、Lambda を試してみたいと思います。

 

 

このサイトからダウンロードできるファイルにはインストーラはなく、単なる ZIP or tar.gz ファイルだけです。なので、適当なディレクトリで展開します。展開すると、JDK と同じディレクトリに構成になります。後は、普通の JDK の使い方と同じです。

ところが、残念なことに、これには ParallelArray クラスや Iterable インタフェースのパラレル処理版の Spliterable インタフェースは含まれていません。なので、一番の目的のパラレル処理を簡単に書くというところは検証できないのです。ちょっと残念ですね。

Project Lambda の仕様

Project Lambda というと、どうしても Lambda 式ばかり注目されてしまいますが、実をいうと Project Lambda の構成要素は Lambda 式以外もあります。

Project Lambda では次の 3 項目について仕様策定を行っています。

  • Lambda 式
  • メソッド参照
  • インタフェースのデフォルト実装

また、この 3 つには含まれませんが、重要な概念として Functional Interface があります。ということで、まず Functional Interface から説明します。

Functional Interface は簡単にいえば 1 つしかメソッドが定義されていないインタフェースのことです。以前は SAM (Single Abstract Method) インタフェースと呼ばれていましたが、現在は Functional Interface と呼ばれています。

たとえば、次のようなインタフェースが Functional Interface です。

interface FileFilter { boolean accept(File x); }

interface Runnable { void run(); }

interface Callable<T> { T call(); }

interface ActionListener { void actionPerformed(ActionEvent event); }

これらの Functional Interface のインスタンス生成を行うのが Lambda 式です。

Lambda 式

さて、一番重要な Lambda 式です。

定義云々はとりあえずおいておいて、先ほどの

        JButton button = new JButton("OK");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // イベント処理
                update();
            }
        });

を Lambda 式で書いてみましょう。

        button.addActionListener( (ActionEvent e) -> update() );

とっても簡単になりました。すばらしい!!

でも、もっと簡単になります。

        button.addActionListener( e -> update() );

静的型付けの Java なのに、関数の引数の型が省略できるのです!!

といっても、動的に型付けを行うのではなく、コンパイル時に型を推論して適切な型を割りふっています。これは Project Coin のダイヤモンド演算子と同じような感じです。

だいたいお分かりだと思いますが、Lambda 式は次のように定義できます。

        (引数1, 引数2, 引数3...) -> メソッド本体

以前はカッコが必要だったり、# が必要だったりしていましたが、現在は () -> だけで Lambda 式を表します。

もちろん、引数の型を書くこともできます。

        (引数1の型 引数1, 引数2の型 引数2, 引数3の型 引数3...) -> メソッド本体

型は省略できるので、あえて書く人は少ないと思いますが...

ちなみに、引数が 1 つの場合はカッコも省略できます。先ほどの ActionListener インタフェースの場合がそうですね。

では、もう少し例をあげてみましょう。

Swing でよく使うあんなコードも...

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame();
                    ...
                frame.setVisible(true);
            }
        });

こうなります。

        SwingUtilities.invokeLater( () -> {
            JFrame frame = new JFrame();
                ...
            frame.setVisible(true);
        });

すごいすっきりしました。なお、引数が 1 つの時はカッコが省略できましたが、引数がない場合はカッコは省略できません。

戻り値がある場合、式が 1 文であれば return は省略できます。1 文以上であれば return を書かなくてはいけません。ここら辺はちょっとやぼったいかも。

        Callable<Boolean> callable1 = () -> true;
 
        Callable<Boolean> callable2 = () -> {
            System.out.println("Call call()");             
            return false;
        };
 
        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<Boolean> future1 = service.submit(callable1);
        Future<Boolean> future2 = service.submit(callable2);
 
        System.out.println("Callable1 result: " + future1.get()
                           + " Callable2 result: " + future2.get());

これを実行すると...

C:\lambda>java CallableTest
Call call()
Callable1 result: true Callable2 result: false

となります。

Functional Interface のメソッドをコールすることもできます。その時は、もともとインタフェースで定義されていたメソッド名でコールします。ここらへんも、普通のクロージャとは感覚が違いますね。

    interface Hello {
        public void sayHello();
    }
 
    public static void main(String[] args) throws Exception {
        Hello hello = () -> System.out.println("Hello, World!");
        hello.sayHello();
    }

実行すると、Hello, World! が出力されます。

このように、Lambda 式を使うととても簡単に記述できるようになります。まぁ、他の言語では当たり前かもしれませんが、Java では画期的ですwww

ところが、よく分らない部分もいくつかあって、その代表的なのが Comparator インタフェース。

Comparator インタフェースは compare メソッドと equals メソッドの 2 つのメソッドを定義しているのですが、なんと Lambda 式で書けてしまいます。Functional Interface の定義とは違うと思うんだけどなぁ...

        // Comparator を Lambda 式で生成
        Comparator<Integer> comparator = (x, y) -> x - y;
 
        System.out.println(comparator.compare(10, 12));
 
        // リストのソートに Comparator を使用する
        List<Integer> nums = Arrays.asList(10, 20, 8, 4, 30);
        Collections.sort(nums, comparator);
        System.out.println(nums);

これを実行するとこうなります。

C:\lambda>java CompTest
-2
[4, 8, 10, 20, 30]

というように、compare メソッドの本体が x - y になっています。どうなっているんだろう?

メソッド参照

つづいて、メソッド参照 (Method Reference)。関数型ではなくて、あくまでもメソッドに対する参照です。といってもたいした使い方ができるわけではありません。

たとえば、FileFilterインタフェースで読み取り専用のファイルをフィルタリングすることを考えます。すると、次のように何種類かの書き方ができます。

        // 読み取り専用のファイルだけをフィルタリングする
        FileFilter filter = new FileFilter() {
            public boolean accept(File f) {
                return f.canRead();
            }
        };

        FileFilter filter2 = (File f) -> f.canRead();

        FileFilter filter3 = f -> f.canRead();

        FileFilter filter4 = File#canRead;

最後の赤字の部分がメソッド参照を利用した書き方です。ようするに引数のメソッドを呼び出すだけの Lambda 式であれば、メソッドを指定するだけでいいということです。

ところが、まだ現状ではこのメソッド参照は実装されていないようです。

追記: Twitter で @bitter_fox さんから File::canReadの型式ではなく、File#canRead の型式でメソッド参照が実装されていると教えていただきました。ありがとうございます。

 

インタフェースのデフォルト実装

ここまで Lambda 式の書き方をいろいろと紹介してきましたが、本命の使い方はまだ紹介していません。

本命とはパラレルに処理するタスクを書くということです。ここでパラレルに処理するタスクとして、一番重要視されているのがループです。

しかし、普通に for 文などでループを書いても、それをパラレルにするのは難しいです。じゃあどうするかというと、内部イテレータを使用します。

内部イテレータは Ruby などの言語で採用されているので、知っている人は知っていると思いますが...

たとえば、コレクションの要素を 2 倍にしたコレクションを作成する場合を考えてみます。

Java の場合
  
    List<Integer> nums = Arrays.asList(0, 1, 2, 3, 4);
    List<Integer> nums2 = new ArrayList<Integer>();
    
    for (Integer num: nums) {
        nums2.add(num * 2);
    }

Groovy の場合

    def nums = [0, 1, 2, 3, 4]
    
    nums = nums.collect { it * 2 }

Groovy での赤で書いた部分がクロージャです。つまり、collect メソッドの引数にクロージャを渡していることになります。

重要なことはこのクロージャがループの中で独立になっているということです。他に依存していないので、この部分はパラレルに処理するのが簡単ということになります。

逆に Java の方は独立かどうかを判断するのが難しいです。

ということで、Java SE 8 からは Java でもコレクションに対して内部イテレータで処理できるようになります。

Oracle の人たちがよく出す例として、2011 年に卒業した人の成績の最大値を出すというのがあります。

今までの Java で普通に書くと、次のようになります。

        class Student {
            public String name;
            public int gradYear;
            public int score;
        }
 
        List<Student> students = ...;
         
        int highestScore = 0;
 
        for (Student s: students) {
            if (s.gradYear == 2011) {
                if (s.score > highestScore) {
                    highestScore = s.score;
                }
            }
        }

これを内部イテレータで記述してみます。

        int highestScore
            = students.filter(new Predicate<Student>() {
                public boolean eval(Student s) {
                    return s.gradYear == 2011;
                }
            }).map(new Mapper<Student, Integer>() {
                public Integer map(Student s) {
                    return s.score;
                }
             }).reduce(0, new Operator<Integer>() {
                public Integer eval(Integer left, Integer right) {
                    return Math.max(left, right);
                }
             });

filter メソッドや map メソッドなど、見慣れないメソッドが登場しました。

filter メソッドはコレクションの要素のフィルタリングをするメソッドです。引数の Predicate オブジェクトの eval メソッドの戻り値が true の要素だけをフィルタリングします。

map メソッドは新しいコレクションを作り直すメソッドです。Mapper オブジェクトの map メソッドの戻り値を新たな要素としたコレクションを作ります。reduce メソッドは要素を減らして、最終的に 1 つにするためのメソッドです。

といっていますが、実際には filter メソッドも map メソッドも戻り値は Iterable インタフェースですがww

ちなみに、Predicate インタフェースや Mapper インタフェースは java.util.functions パッケージで提供されています。

では、これを Lambda 式で書きかえてみます。

        int highestScore
            = students.filter( s -> s.gradYear == 2011 )
                      .map( s -> s.score )
                      .reduce(0, (left, right) -> Math.max(left, right));

はじめの for 文と if 文で書いていたのにくらべ、すごくスッキリしました。しかも、分りやすい。

では実行してみましょう。適当な Student オブジェクトを作って、実行すると次のようになりました。

C:\lambda>java IteratorTest
98

ちなみに reduce メソッドの Lambda 式をメソッド参照を使用して書くこともできます。

次に、これをパラレルに行うにはどうすればいいか。それもすごい簡単です。

        int highestScore
             = students.parallel()
                       .filter( s -> s.gradYear == 2011 )
                       .map( s -> s.score )
                       .reduce(0, (left, right) -> Math.max(left, right));

なんと、赤字で書いたように parallel メソッドを間に挟むだけです。ここではこれ以上触れませんが、機会があればここらへんもまた紹介したいとお思います。

さて、こういうようにコレクションに対するイテレーションを Lambda 式で書けることは分りました。しかし、問題はすべてのコレクションに filter メソッドや map メソッドを付け加えなければいけないということです。

しかし、たとえば Iterable インタフェースに filter メソッドや map メソッドを追加してしまったらどうなるでしょう。

インタフェースを実装するクラスは、インタフェースで定義したメソッドは必ず定義しなくてはなりません (もちろん抽象クラスは違います)。ということは、安易に Iterable インタフェースに filter メソッドなどを追加してしまうと、非常に多くのクラスが影響を受けてしまいます。

もちろん、自作のクラスもこの影響を逃れえません。

つまり、昔のソースをコンパイルし直したら、エラーだらけということになってしまうかもしれないのです。

しかし、これは Java のコンセプトとは食い違います。Java の一番重要なコンセプトは Compatibility is King です。コンパチビリティが常に優先されます。

しかし、Iterable インタフェースにメソッドを追加したら、これが壊れてしまうわけです。

では、どうするか?

その答えが、インタフェースのデフォルト実装です。

インタフェースの実装クラスが、インタフェースで定義したメソッドを実装していない場合、デフォルト実装が使われることになります。

デフォルト実装をどのように記述するかは、実際のコードを見てみましょう。Project Lambda を含んだ JDK にも src.zip が提供されており、それを解凍すると Iterable インタフェースなどを見ることができます。

以下に Iterable インタフェースを示します (コメントは省略しました)。

public interface Iterable<T> {
    Iterator<T> iterator();

    boolean isEmpty() 
        default Iterables.isEmpty;
        
    Iterable<T> filter(Predicate<? super T> predicate) 
        default Iterables.filter;
        
    Iterable<T> forEach(Block<? super T> block) 
        default Iterables.forEach;
        
    <U> Iterable<U> map(Mapper<? super T, ? extends U> mapper) 
        default Iterables.map;
        
 
         <<以下、省略>>        
}

赤で示したように、メソッドの定義の後に default を記述し、その後にデフォルトで使用するメソッドを記述します。ただし、ここで指定できるメソッドは static メソッドだけです。

ちなみに、デフォルト実装で使用している Iterables クラスも Java SE 8 で導入されたクラスで、java.util パッケージにあります。

このようにデフォルト実装を導入することで、既存のソースコードに影響を与えずに、インタフェースに機能を追加することができるのです。

終わりに

とても長くなってしまったのですが、最後にまとめておきます。

Project Lambda が仕様が決まるまで非常に長い道のりがありました。ここに来てやっと、仕様がまとまってきたのは、目的がキチンと決まっていることです。

その目的をもう一度書いておくと

パラレルに処理するタスクを、簡単に記述すること

です。

そのために、取った導入した手段が

Functional Interfaceのインスタンス生成を簡単に記述する Lambda 式

です。

これ以外に、次の機能も導入しています。

  • メソッド参照
  • インタフェースのデフォルト実装

クロージャをもとめていた人にとっては Project Lambda はものたりないものかもしれません。しかし、Project Lambda の本来の目的と、そして Compatibility is King という Java コンセプトを考え合わせると、現在の Project Lambda の仕様は妥当であると感じています。

(個人的には Straw-Man Proposal から関数型が抜けた時は残念に思いましたけど ^ ^;;)

そして、使ってみると分るのですが、意外に使えます。ぜひ、使ってみてください。

ちなみに、Java SE 9 以降で関数型復活という噂もあるので、Java SE 8 で Project Lambda が完結するのではなく、9, 10 と機能の向上を続けていくのだと思います。

しかし、どうしてもクロージャを使いたいというのであれば、JRuby なり Groovy なり Scala なりを使えばいいと思いますww

今日の一枚

このエントリーをはてなブックマークに追加

Kenny Burrell "Have Yourself a Soulful Little Christmas" (1966)

今年も 12 月はクリスマスアルバムです。

1 枚目はKenny Burrell の定番アルバム。櫻庭は Jazz ギターはあまり聞かないというか、ほとんど聞かないのですが、これはいいですよ。

ちょうど 60 年代の Chicago で録音されたので、シカゴブルースやソウルが出てきたころと重なるわけです。そういうにおいがしてきますね。タイトルからして Soulful だし。

そして、Jazz の醍醐味といえばインプロビゼーション。1 曲目の The Little Drummer Boy から、アドリブをばんばん聞かせてくれます。それをサポートするホーンもカッコいい。

Jazz の定番でもある My Favorite Things が一番かな。アップテンポの曲の方が Kenny Burrell のギターが活きてくるように感じます。惜しむらくは曲が短い。3 分半は短いですよ。10 分ぐらいやってくれないかなぁ。