技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

java8:関数型インターフェースの背景にある考え方

【前置き】
Java8から関数型インターフェースが使用可能になりました。
具体的に「ラムダ式」「Stream」「Optional」「Files」と言った方がわかりやすいでしょうか。
 
関数型インターフェースは関数型プログラミングをサポートするものであるため、従来からJavaでサポートされていたオブジェクト指向プログラミングとは発想が異なります。
Javaを使っている人は従来のオブジェクト指向プログラミング的な書き方には慣れていると思うのですが、関数型インターフェースには正直抵抗感を覚えると思います(私も慣れていないので抵抗感があります)。何をしているのかわからないという次元かもしれません。
しかし、関数型インターフェースの使用を半ば強制されるフレームワークが登場していたり(例:Apache Spark)、関数型インターフェースでJavaを書く開発者も見かけるようになってきたため、いつまでも関数型インターフェースから逃げるわけにもいかないというのが実情だと思います。
 
関数型インターフェースに抵抗感を覚えるのは、その背景にある関数型プログラミングの発想を知らないからだと思いました。
そこで、今回は、自分なりに理解した関数型プログラミングの考え方を紹介したいと思います。
 
【サンプルコード】
言葉で説明するよりも先にサンプルコードを見た方がわかりやすいと思うので、サンプルコードを先に紹介します。
年齢のリストから30代の人数を数える、というプログラムです。
ごく短いプログラムですので、お付き合いください。
 
・FunctionTest.java

import java.util.Arrays;
import java.util.List;

public class FunctionalTest {

    public static void main(String args) {
        Integer
age = {38, 26, 40, 32, 36};
        List<Integer> list = Arrays.asList(age);

        System.out.println("手続き型として記述");
        int count = 0;
        for (int i = 0; i < list.size(); i++) {
            int individualAge = list.get(i);
            if (individualAge >= 30 && individualAge <= 39) {
                count++;
            }
        }
        System.out.println(count);

        System.out.println("関数型インターフェースを使用");
        System.out.println(list.stream()
                               .filter(x -> x >= 30 && x <= 39)
                               .count());

    }

}
 
・実行結果
手続き型として記述
3
関数型インターフェースを使用
3
 
【関数型プログラミングの考え方】
関数型プログラミングでは、以下のことを実現しようとしています。
色々難しい用語(例えば「副作用」等)はあるのですが、今回は用語を使わずに簡潔にまとめます。
 
・内部状態(State)を排除する
 最も本質的な考え方です。
 
 関数型プログラミングでは、内部状態を排除することを目的としています。
 「内部状態」とは、上記のコードで言うと「count」や「i」を指します。
 
 内部状態が入りこんでしまうと、
 内部状態により関数の結果が変わってしまうため、
 内部状態を把握する必要が出てきてしまい、可読性が悪化します。
 (把握のために「count」や「i」をトレースする必要が出てきてしまう)
 把握しきれずに意図しないバグを出してしまうことも珍しくありません。
 内部状態を排除して、品質を上げよう、という発想です。
 
 また、コンピュータにとっては内部状態は重要ですが、
 人間にとってはやりたいことを実現できれば良く、
 内部状態は重要ではありません。
 重要ではない記述を削減することでコードを完結にしたい、
 という発想もあります。
 
 Java8のラムダ式では、ラムダ式の外部で定義された変数の値を
 ラムダ式の内部で変更することを禁止されています(コンパイルエラーになる)。
 その背景には、内部状態の排除があると思っています。
 
・自然言語に近い形で処理を記述する
 これは、コードが簡潔になった結果生じた副次的な考え方かもしれません。
 
 関数型プログラミングでは、関数を組み合わせることにより処理を実現します。
 関数を次々とつなぎ合わせるように記述することで、
 ソースコードが自然言語に近い形になります。
 わかりやすく言えば、ソースコード自体がコメントのようになります。
 
 例えば、サンプルコードでは
 「年齢のリストから30代の人数を数える」
 という処理を行おうとしています。
 従来のプログラミングでは、
 これを実現するためにforループとかカウント用の変数を使用しており、
 何をしているのか把握するためには、
 内部状態をトレースして意図を汲み取る必要があります。
 しかし、関数型プログラミングでは、
 「list.stream().filter(x -> x >= 30 && x <= 39).count()」→
 「listを30<=x<=39でfilterしてcountする」
 と読めるため、
 「年齢のリストから30代の人数を数える」
 という処理であることを自然に把握することができます。