※当サイトの記事には、広告・プロモーションが含まれます。

Java 8 で関数型インターフェイスって導入されたけど...カリー化って? 部分適用とは異なるらしい

f:id:ts0818:20190629125053j:plain

将棋でもっとも大事なのは、一つの場面で正確な判断を下すことだ。その基準を瞬時に見いださなければならない。

対局中には、これまでの経験から、たくさんの知識がばらばらに思い浮かぶ。

だが、それらはジグソーパズルのピースのようなものなので、そのままでは役に立たない。それを「これはいい手だ」「悪い手だ」とか「どういう流れだ」と瞬時に判断し、次の一手を選択しなければならない。

方向性やプランに基づいて、ばらばらの知識のピースを連結するのが知恵の働きである。

読書メモ: 羽生 善治「決断力」|砂流って読めますか?|note

⇧  『決断力(著:羽生善治)』の一節ですね 、あっ!どうも、ボクです。

複数のものを、まとめ上げていく、その行為を、ジグゾーパズルになぞらえているようですね、名人!

というわけで、今回は、Java のお話です。

 

関数型インターフェイスって?

そもそも、関数型インターフェイスとは?

qiita.com

⇧   上記サイト様が詳しいです。

また、

www.ibm.com

ラムダ式の型は何なのでしょう?一部の言語では、関数の値や関数オブジェクトを使ってラムダ式を表現しますが、Java 言語では違います。Java では、関数型インターフェースを使用してラムダ式の型を表現します。初めは奇妙に思えるかもしれませんが、実のところ、これは Java の古いバージョンとの後方互換性を確実にする効率的な方法なのです。

Java 8 のイディオム: 関数型インターフェース

⇧  上記サイト様によりますと、「関数型インターフェイス」っていうのは、Java 独自の仕様らしいですね。

Java 8 以前との互換性のために、こういう形で導入されたようです。

 

Oracle JDKOracleさんのドキュメントの説明では、

docs.oracle.com

関数型インタフェースは、ラムダ式やメソッド参照のターゲットとなる型を提供します。

各関数型インタフェースには、その関数型インタフェースの関数メソッドと呼ばれる単一の抽象メソッドが含まれており、ラムダ式のパラメータや戻り値の型のマッチングや適応は、そのメソッドに対して行われます。

関数型インタフェースは代入の文脈、メソッド呼び出しの文脈、キャストの文脈など、さまざまな文脈でターゲットの型を提供できます。

java.util.function (Java Platform SE 8)

⇧  ラムダ式やメソッド参照を実装する上で、必要になってくる型として存在しているのだと。 

まとめると、関数型インターフェイスとは、

ってことですかね...よく分からんけど。

 

ラムダ式、とか、メソッド参照、とか...って何なの?

そんじゃあ、「ラムダ式」、「メソッド参照」って何なのよ?

Wikipediaさんによりますと、

プログラミング言語における無名関数英語anonymous functionあるいはnameless functionとは、名前付けされずに定義された関数のことである。無名関数を表現するための方法には様々なものがあるが、近年主流となっているのはラムダ式による記法である。無名関数を表現するリテラル式は、関数リテラル (function literal) とも呼ばれる。値がある場合は関数オブジェクトであるものが多い。

無名関数 - Wikipedia

⇧  「無名関数」の1種であるらしいと。要するに、関数らしい。

なので、

ラムダ式」∈「無名関数」

 ってことが言える?ということで、「ラムダ式」は、関数であるから、Javaでは「関数型インターフェイス」を実装する必要があるってことになるらしい。

つまり、「関数型インターフェイス」が、何故にJava 8 で登場したかというと、Javaラムダ式を実現したかったからと言っても過言では無いのではないかと。

C# とかは、ラムダ式が早い段階で実現されていたらしい。

関数型ではないプログラミング言語においても、ラムダ式を言語機能として取り入れる動きが活発である。

C#ではC# 3.0にて導入された。

C++ではC++11にて導入された。

JavaではJava 8にて導入された。

無名関数 - Wikipedia

 

では、「メソッド参照」は? 

yohhoy.hatenadiary.jp

⇧  上記サイト様が詳しいです。

 

で、どういう時に「メソッド参照」を使うのよ?

ww.ibm.com

関数型スタイルのプログラミングでは、ラムダ式を匿名関数として渡し、ラムダを高階関数の引数として使用するのが一般的な方法となっています。

Java 8 のイディオム: パススルーに代わる手段

⇧  と前置きして、

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
numbers.stream()
  .filter(e -> e % 2 == 0)
  .forEach(e -> System.out.println(e));

Java 8 のイディオム: パススルーに代わる手段

⇧  上記のような例を提示し、

forEach メソッドに渡されるラムダ式が、パススルー・ラムダ式と呼ばれるものです。式 e -> System.out.println(e) はそのパラメーターを、PrintStream クラスの println メソッド (System.out のインスタンス) に引数として渡します。

Java 8 のイディオム: パススルーに代わる手段

⇧  こういったケースのラムダ式においては、

パラメーターで実際の作業を行わないパススルー・ラムダ式は、メソッド参照で置き換えるほうが有益です。

Java 8 のイディオム: パススルーに代わる手段

⇧  ということらしく、 

numbers.stream()
.filter(e -> e % 2 == 0)
.forEach(System.out::println);

Java 8 のイディオム: パススルーに代わる手段

⇧  のように、置き換えたほうが可読性も上がるでしょう、ということらしい。

ラムダ式」だと冗長になりそうな時~、「メソッド参照」を使うべしってことかしらね...。

  

カリー化って?部分適用と混同されやすいらしい...

関数型インターフェイスが、「ラムダ式」や「メソッド参照」を実現する、つまり、Java関数型プログラミング的なことを実現するためのものとして導入されたんであると。

ということは、「カリー化」ができてしまうというわけですか?

カリー化って何じゃらほい?

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

カリー化 - Wikipedia

⇧  何だろう、説明がむちゃくちゃ分かりにくいんだけど...

 

d.hatena.ne.jp

カリー化は、複数引数を取る関数(例えばAとBを取ってCを返す関数)を、一つの関数を取って「関数」を返す関数にすること(Aを取って「Bを取ってCを返す関数」を返す関数)です。
カリー化したところで、AとBを渡さないとCを得ることはできません。
それに対して、部分適用はAとBが必要な関数に例えばAを(先に)渡してしまって、Bを取ってCを返す関数にすることです。

Java7でカリー化?(部分適用でした、、、) - tomoTakaの日記

⇧ 上記サイト様のコメントの方の説明が分かりやすいです。

具体例としては、

qiita.com

⇧  JavaScriptのコードですが、上記サイト様のコード例が、「カリー化」とはどういうものかを理解するのに、分かりやすいです。

 

Java でカリー化

というわけで、Java 8 で関数型インターフェイスが導入され、「カリー化」が実現できるということなので、試してみますか。

 

taro.hatenablog.jp

⇧   上記サイト様を参考にさせていただきました。

 

Eclipse を起動し、適当なJavaプロジェクトを作成し、クラスも作成で。

f:id:ts0818:20190629175157p:plain

ソースはこんな感じになりました。 

package main;

import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;

public class TestCurry {
  
  public static void main(String[] args) {
    // カリー化のメソッドを実装
    Function<Integer, Function<Integer, Integer>> sum = curried(TestCurry::sum);
    // Optionalな値を用意
    Optional<Integer> a = Optional.ofNullable(80);
    Optional<Integer> b = Optional.ofNullable(20);
    Optional<Integer> c = Optional.empty();
    
    // Optionalな値で関数を実行するメソッドの結果を出力
    System.out.println(apply(apply(Optional.of(sum), a), b));
    System.out.println(apply(apply(Optional.of(sum), a), c));
    System.out.println(apply(apply(Optional.empty(), a), b));

    // Optionalな値で関数を実行するメソッドの結果を値にして出力
    System.out.println(apply(apply(Optional.of(sum), a), b).orElse(0));
    System.out.println(apply(apply(Optional.of(sum), a), c).orElse(0));
    System.out.println(apply(apply(Optional.empty(), a), b).orElse(0));

    // カリー化のメソッドのみを使用した結果を出力
    System.out.println(sum.apply(a.orElse(0)).apply(b.orElse(0)));
    System.out.println(sum.apply(a.orElse(0)).apply(c.orElse(0)));
  }
  
  // カリー化のメソッド
  static <T, U, R> Function<T, Function<U, R>> curried(BiFunction<T, U, R> biFunction)  {
    return (t) -> (u) -> biFunction.apply(t, u);
  }
  
  // 合計するメソッド(カリー化の対象メソッド)
  static int sum(int a, int b) {
    return a + b;
  }
  
  // 渡された関数を、渡された引数(Optionalな値)で実行するメソッド
  static <A, B> Optional<B> apply(Optional<Function<A, B>> func, Optional<A> param) {
    return param.flatMap((p)-> func.map((f) -> f.apply(p)));
  }
}

んで、実行してみる。

f:id:ts0818:20190629175239p:plain

f:id:ts0818:20190629175315p:plain

orElse() に、「0」を入れてるのは、「null」を入れてしまうと、2回目のapplyメソッドの実行で、null.apply() で実行される可能性がある、つまり、NullPointerException で落ちるからですかね。まぁ、このへんの実装も、う~ん、な気がしますが、他に思いつかんかった...

まぁ、何て言うか、関数型インターフェイスの記述、慣れる気がしない...

今日もまたモヤモヤ感が...

今回はこのへんで。