Skip to the content.

Java による関数型プログラミング

Hello、ラムダ式!

FP の利点

Stream v.s. Iterator

後者は万能だが、「多芸は無芸」。前者は、合計値を得る、条件によって抽出する、別の型の object に変換する、等、目的に特化した API を備えており、可読性に寄与する。

高階関数によるポリシーの強制

関数を第一級市民として扱うことが可能になったことで、処理を他の箇所に注入するということが気軽にできるようになり、関心の分離に置いて有利。
所謂 Strategy pattern と一緒だが、記述すべきコード量が段違い。

コレクションの使用

はい。

文字列、コンパレータ、フィルタ

文字列

String#charscharStream を得られる。

コンパレータ

純粋なコンパレータだけじゃなくて、差を計算する method とかを流用することもできる。 例えば、Person というクラスに ageDifference(Person):int みたいな method があれば、年齢による sort はこれを利用しさえすればよい。 Comparator.compareing(Person::getAge) とかでもよい。

コレクタ

Collectors の中の toList toSet toMap joining mapping collectingAndThen minBy maxBy など。

フィルタ

File#listFiles でディレクトリにある全ファイルを含む Stream を生成できる。ファイル名だけ欲しいなら File#list
Files#list(Path) は、指定したパスに存在するファイルを列挙する。
flatMap: モナド合成

ラムダ式で設計する

外部リソースを扱う

StreamAutoClosable interface を実装している。これによって、File#lists とかで取得した外部リソースを扱う Stream であったとしても、trye-with-resource を使って自動的に close することができる。
Java 8 以前は、AutoClosable を実装する class は必ず close されなければならないという一般契約だったが、Stream は外部リソースを扱うか否かにかかわらず AutoClosable を実装した class である -> Java 8 以降は AutoClosable の契約が変わっており、必ず close されなければならないわけではなく、close される可能性がある、というものに変更されている。

execute around method pattern

Smalltalk 時代からあるデザインパターンのひとつ。これ がそこそこわかりやすいかも。
つまり、今回でいえば FileWriter で何かを execute するときに、高階関数を導入して、Consumer<FileWriter> みたいな操作を外から受け取って、その前後に何か(今回で言えば FileWriter#close)を挟み込む、みたいなやつ。DI 的というか、Strategy pattern っぽいというか、Proxy pattern っぽいというか、まあそんな感じ。パターンというよりかは実装手法に近い感じかも。
これによって、例えば Closable の instance を使うときに開発者が close し忘れるなどの問題が解決する。

「遅延させる」ということ

Singleton pattern

class Foo {

  private Supplier<Foo> foo = Foo::createAndCache();
  
  private Foo() {
  }
  
  public Foo getInstance() {
    return foo.get();
  }
  
  private synchronized Foo createAndCache() {
    class FooFactory implements Supplier<Foo> {
      private final Foo fooInstance = new Foo();
      public Foo get() { return fooInstance; }
    }
    
    if(!FooFactory.class.isInstance(foo)) {
      foo = new FooFactory();
    }
    
    return foo.get();
  }
  
}

無限 Stream

  Stream.iterate(2, Primes::smallestPrimeGreaterThanOrEqualTo)
      .limit(lim)
      .toList();

みたいな。

再帰の最適化

前の章で見た無限 Stream を応用すると、末尾再帰を一般化できる。

class Math {

  static int factorial(int num) {
    return factorial(1, num).invoke().intValue();
  }

  private static TailCall<Integer> factorial(int fac, int num) {
    if(num==1) return done(fac);
    else call(() -> factorial(fac*num, num-1));
  }
  
}
@FunctionalInterface
interface TailCall<T> {

  TailCall<T> apply();
  default boolean isComplete() { return true; };
  default T result() { throw new UnsupportedOperationException("not complete"); }
  default T invoke() {
    return Stream.iterate(this, TailCall::apply)
        .filter(TailCall::isComplete)
        .findFirst()
        .get()
        .result();
  }
  
}
class TailCalls {

  static <T> TailCall<T> done(T t) {
    return new TailCall<T>{
      @Override boolean isComplete() { return true; }
      @Override T result() { return t; }
      @Override TailCall<T> apply() { throw new UnsupportedOperationException("already complete"); }
    }
  }
  
  static <T> TailCall<T> call(TailCall<T> next) { return next; }
  
}

※ 多分 Scala のトランポリンと同じ話。

ラムダ式で合成

すべてをまとめて

ホームへ戻る