Edited at

関数型プログラミングって何、ラムダってなんだよ

More than 1 year has passed since last update.


java8で追加されたラムダ式とストリーム処理がわからない

ラムダとストリームはjava8から新しく導入された機能ですね

そして自分自身javaを使ってきてどうしても馴染めない機能でもあります

ラムダ式とストリーム処理って実は何かわからない

関数型プログラミングって最近よく聞くけど何みたいな感じで

なので、ちょっとしたメモ程度で書いていきます


関数型プログラミングの「関数」ってなに

関数型プログラミングというぐらいなので、関数に関係してるものだと言うのは誰でもわかると思います。

ですが、大切なのは関数が具体的に何かということより

その関数が備える性質の方です

関数に必要な性質は、ざっくり言えば

入力に大して出力がただひとつに定まることです。

プログラミングでいうなら入力とは引数出力とは戻り値です。

なので、javaで言うなら

関数とは引数と戻り値を持つメソッドなんだな程度で理解しておくといいと思います


関数型プログラミングのメリット

関数とはなにか大体わかったところで関数型プログラミングを行うメリットとは

・記述自体が簡潔になることが多く、コードが読みやすくなる

・テストがしやすくなる

・関数同士を組み合わせても相互に影響しない

・渡す値にある程度見通しを持てるためバグを少なくできる

などなどあります


javaで関数型プログラミングを行うと

javaですべて関数型で書くのはあまり現実的ではありません

というのも基本的に命令型をベースにしているからです。

ですが、関数型プログラミングをいいタイミングで用いるとコードが読みやすくなったり実行効率がよくなったり…


関数型インターフェースを理解する

関数型インターフェースを理解すると関数型プログラミングの理解がしやすくなるので紹介しておきます

関数型インターフェースとは抽象メソッドを一つ持つインターフェースのことです。

例えば…


@FunctionalInterface
interface Hello{
public void sayHi(String name);
}

みたいなインターフェースのことです

ただ厄介なのは関数型インターフェースにならない時があることです

それは以下のコードを参照してください


interface Ex{
String toString();//こういったObjectクラスのメソッドのオーバーライド
}

interface Ex2{
default void sayHi();//こういったdefaultメソッド
}

さらにめんどうなのは、関数型インターフェースに見えなくても、関数型インターフェースになってしまう以下のような場合です


@FunctionalInterface
interface Ex3{
public void sayHi(String name);
default void sayGoodBye();
String toString();
}

まずこの場合抽象メソッドとして扱われるのは一番最初に定義したメソッドで

それ以外は、上記の関数型インターフェースにならない条件を持つメソッドのため全体で抽象メソッドがひとつだけとなり事実上、関数型インターフェースになります


メソッド参照

メソッド参照もまたjava8から追加された機能です

ざっくり言うとメソッド単体を参照する仕組みです

文法は以下のようになっています

対象
文法

クラスメソッド
クラス名::クラスメソッド名
String::toString

インスタンスメソッド
オブジェクト参照::インスタンスメソッド名
System.out::println

インスタンスメソッド
型名::インスタンスメソッド名
StringBuilder::append

コンストラクタ
クラス名::new
String::new


ラムダ式

やっとラムダ式の説明まで来ました。

まずラムダ式とはなにかということに触れておきましょう

ざっくり言えば…

ラムダ式とはメソッド定義を式として扱える機能のことです

メソッド定義を式として扱うのはいいが、どのメソッドを扱うんだと言う方のために

「なんの為に関数型インターフェースを紹介したのでしょうか」

そうです、関数型インターフェースで宣言したあの抽象メソッドを式として宣言します

もう関数型インターフェースも紹介してしまったので先にコードで例を示します


exlambda.java


public class exlambda{
public static void main(String... args){
hello h = (String name) -> { //ここの段階で抽象メソッドのメソッド定義を式として扱う
return "hello"+name;
};

System.out.println(h.sayHi("duke"));//出力:hello,duke
}
}

@FunctionalInterface
interface hello{
public String sayHi(String name);//抽象メソッド
}


コードを見ればわかると思いますが実は使ってみると意外と簡単だったりします


ラムダ式の面白い文法

まずベースとなる文法は

(仮引数列) -> {処理};

です。

仮引数列がメソッドの仮引数で処理がメソッドの本体となります

実はさっきのコード(exlambda.java)はもっと省略できて

ラムダ式の定義の部分だけ見ると

(name) -> {return "hello,"+name};//引数のデータ型を省略出来る

name -> "hello,"+name; //処理を囲むカッコも省略出来る、returnも。引数の丸括弧が省略出来るのは引数がひとつの時だけ
() -> "hello,world"; //引数がないときはこう、丸括弧は省略できない
name -> {return "hello,"+name;}; //処理をかっこで囲んだ場合、文にする必要がある。処理は複数行定義出来る

こういう風に書くと結構コードが見やすくなります

ですが、これを知らない人から見るとわけがわからないのでコメントをつけたりむやみに乱用しないほうがいいでしょう


ラムダ式で出るエラー

関数型インターフェースの抽象メソッドと引数の型や戻り値の型が合わない

上記の省略を適切にしようしていない

などなど…

基本的文法に反する場合は容赦なく今までどおりエラーを吐いてきます


ラムダ式とスコープ

ラムダ式は定義方法が特殊なため少しだけスコープの知識が必要になります


ラムダ式とthis

あまり推奨はされないが普段通り参照できる

つまりフィールド変数については今まで通りアクセス出来る

もっと言えばインスタンスメソッドにもアクセス出来る


ラムダ式とローカル変数

ローカル変数の扱いは少しだけ厄介で、注意が必要です

まずラムダ式ではローカル変数を実質finalとして扱います

なので、ローカル変数にアクセスし値を変更(インクリメントなど)をしようとするとエラーになります

再代入も同じくエラーになります


ラムダ式と変数のスコープ

ラムダ式の中には変数を定義することができます

しかしラムダ式にはパラメータ変数や式内のローカル変数のスコープはラムダ式で閉じるという仕様があります


ラムダ式とクロージャー

javaでは完全ではないものの、メソッド内にメソッド定義をした時内側のメソッドが外側の環境を保持するクロージャーという仕組みがあります

これがあるためにローカル変数の変更や再代入はエラーになります

逆にこれがあるためにローカル変数は実質finalとして扱われるのでしょう


java.util.function

これはラムダ式を扱う上で非常に便利な標準関数型インターフェースの集まりです

私が解説するよりもよくまとまっている投稿があったのでお借りします

java.util.function


ストリーム処理

ストリーム処理とは、繰り返し処理のことです

コレクションで使うイテレータと似ています

具体的な処理構造は


  • データソースから初期ストリームを生成

  • 複数の中間処理はストリームからストリームへの変換処理として記述

  • 一つの終端処理はストリームの最終出力処理

と、いってもよくわからないので実例を踏まえて理解することにしましょう

まず細かく手順を明確にふんだコードで例を示します

解説はコード内のコメントを読んでください


exStream.java


import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class exStream{
public static void main(String... args){
List<String> list = Arrays.asList("c","java","js","python","scala","php","ruby");//ストリームを行う対象
Stream<String> s1 = list.stream();//初期ストリーム生成
Stream<String> s2 = s1.filter(s -> s.contains("a"));//中間ストリーム処理。filterの引数はラムダ式、ここではaを含むもの
s2.forEach(System.out::println);//ストリームの終端処理
}
}


これで例は示せたものの処理ごとにStreamを定義していたのでは効率的だとは言えません

効率化の手段として以下のように書き直せます


exStream.java


import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class exStream{
public static void main(String... args){
List<String> list = Arrays.asList("c","java","js","python","scala","php","ruby");//ストリームを行う対象
list.stream()
.filter(s -> s.contains("a"))
.forEach(System.out::println);
}
}


言わなくてもわかるとは思いますが.の前で改行しているのは見やすくするためです

一行で書いてしまってもいいのですがそれだととても見にくいので改行しました


これでとりあえず基本は終わり

ここまでで、ラムダ式やストリーム処理などの基本的なことの紹介兼メモは終了です

実際はもっと深いのですが一度に全部紹介してしまうと量が多いので

後々公開していきます