2019.11.03 初投稿以降1年を経過しましたので内容の本質を変えず、提示ソースの説明追加を中心にレビュー・更新しました。
2021.01.21 「実際Javaシステム開発においてどの程度関数型プログラミングを取り込むべきなのか?」を中心テーマとして第2部を投稿しました。
第2部へのリンク
2022.07.14 「分かりにくいmap,flatMap,collectの追加紹介と知っておくと便利なCollector,Collections,Arrays」をテーマとして第3部を投稿しました。
第3部へのリンク
はじめに
Java8が2014年3月にリリースされてから4年以上がたちJava9やJava10以降を含め多くの方々に利用されています。Java8リリース当時話題となったのが関数型プログラミングです。現在では関数型プログラミングという言葉はシステム開発関係者の間では一般的に認知され浸透しています。
しかし、現状関数型プログラミング主体のシステム開発実績を頻繁に目にするという状況ではないことから実際の浸透はまだまだ先の事だと思われます。そして、Java8もそうですが既存言語も関数型プログラミングスタイルを取り入れて機能強化を進めています。
今後どういう流れになっていくかは予測できませんが、個人的には既存言語の中で関数型プログラミングスタイルを使っていくというのが主流になるのではと推測しています。純粋な関数型言語は特化した使い方になり、Java系で言えばScalaがJava言語にとって代わるという状況にはならないだろうと考えています。
このような状況にあると思える関数型プログラミングですが、今後何らかの形で浸透いくと予想されるので、今さらながらですが、全体調査を行い、Java10環境で簡単なプログラミングを実践学習してみました。
また、興味ある方の何らかの参考になるのではと思い備忘録も兼ねて学習結果を投稿することにしました。なお、内容には主観にもとづく部分や、個人的指向が入ったプログラムスタイルになっている場合もありますので了承お願い致します。
関数プログラミング特有の語句については後尾部分に理解した範囲での簡単な説明を記述していますので参考にして下さい。(*番号が付記された語句)
追加されたJava8新機能
Java8では関数プログラミングに関して以下の新機能が追加されています。
- 関数型インターフェイス(*1)
- ラムダ式(*2)
- StreamAPI(*5)
- Optional(*6)
- Map/List/Setの一部メソッド
これらの紹介に入る前にそもそも関数型プログラミングとはどういうものなのかについて学習した範囲で簡単に説明を試みたいと思います。
関数型プログラミングとは
関数型プログラミングとは狭義では「参照透過性(*9)を持った副作用(*8)のない関数によってプログラムを組み立てていく手法」となっており、この定義では関数型言語がベースとなるのですが、現在では既存言語でも関数型プログラミングスタイルでの実装が可能となっており、ここでは関数型プログラミングを広域的な視野でとらえることとし、その特徴を3点挙げてみました。
- 関数の引数に関数(ラムダ式など)を渡せる機能を活用してプログラミングの簡素化 を行う。
- 言語・APIにより提供されている関数型プログラミング用各種メソッド(関数)や文法等を活用してプログラミングの簡素化や品質の向上を行う。
- 参照透明性(*9)のある副作用の無い関数を作成・活用し、その関数を組み合わせてシステムの大部分を開発し、安全で高品質なシステムを目指す。なお、問題対象を数学的に表現できればほぼ全てを関数で開発することができると一般的には言われています。
上述のような方法・目的でプログラムを作成していくことが広い視野での関数型プログラミングと認識できると思います。また、関数型プログラミングはオブジェクト指向のように要求分析設計工程といった開発手法と連携したものでなく、それ自体はプログラム実装工程が対象となっています。
関数型プログラミングを行うために、HaskellやScalaをはじめとした言語レベルで対応したものや、Java8のように既存言語に関数型プログラミング用APIを付加したもの等があります。
Java8は1.~2.までの範囲を、HaskellやScalaは1.~3.までの範囲をカバーできていると言われています。副作用をほぼ避けることができるプログラミング言語は純粋関数型プログラミング言語とも呼ばれており、この点から言えばScalaの3.は純粋な3.というより準3.というレベルだと思われます。ただ、システムを「副作用をほぼ避けた関数」で構成する必要があるかはそのプロジェクト次第である事は言うまでもありません。
関数型プログラミングは比較的小さな関数を作りこれを適用あるいは組み合わせてシステムを開発していくというような手法をとっており、宣言型プログラミング(*3)形式ベースとなっています。従って、3.のレベルを実装するためには従来のオブジェクト指向プログラミング手法ではなく関数型言語および関数型プログラミングの考え方を十分理解しておく必要があります。
また、対象問題がどの程度関数型プログラミングにマッチしているのか、連携するシステム、基盤ミドルウェア、基盤フレームワーク、関連蓄積資産、開発・管理・運用環境といった面にも事前に十分把握しておく必要があります。
関数型プログラミングのメリットとして以下の項目が一般的には挙げられています。
- プログラミング量が減る。
- 品質が向上する。
- テストをしやすい。
- 高速化に寄与する。
- 並列化がしやすい。
4番目及び5番目の項目については多少ケースバイケースという状況があるように思えますが、適用部分についてプログラミング量が減少するというのは確かだと思われます。
一方欠点としては、開発システムが全て数学的に表現できるのであれば理論上は純粋な関数プログラミングで開発可能なのですが、現実的にはそのような環境は少なく、特にビジネス系システムでCRUD処理がメインとなるようなアプリケーションでは余りメリットが無いのではと言われています。
Java8では、関数型プログラミングに対応するために関数型インターフェース、ラムダ式、Stream、Optionalなどの仕組みが新たに導入されました。これにより従来の命令型プログラミングの一部が関数型プログラミング(宣言型プログラミング)スタイルで記述することが可能になりました。
そしてこのJava8以降で関数型プログラミングを行う時には、
- オブジェクト指向プログラミングをベースにして可能な範囲で新規APIを使っていく。(主としてコーディング量の減少)
- もう少し進んで、参照透明性のある(できれば副作用の無い)関数を可能な範囲で作成・活用してメソッドを実装していく。(コーディング量減少と品質の向上)
の2通りのケースがあると思いますが、個人的には(1)→(2)とステップを踏んで習得していくのが良いかと思います。
なお、気を付けなければいけないことは関数型プログラミングはJava8以降でしか稼働しないということです。Java6やJava7等の旧Javaバージョンでも稼働可とするには関数型プログラミングではなく、これまで通りの命令型プログラミングで開発する必要があります。
Java8新機能の概説
Java8において関数型プログラミングに関連して新規・更新された主なメソッドは以下の通りです。これらのメソッドを使用して関数型プログラミングの基本的な機能が利用できるようになっています。オブジェクト指向言語に関数型プログラミングを取り入れたという表現ができるのではないかと思います。
これら各メソッドの詳細はネットや書籍で簡単に調べることができます。
ここでは各メソッドの概説と代表的なメソッドについてサンプルを提示していきます。
・関数型インターフェイス
関数型インターフェイスを使用することによりラムダ式やメソッド参照(*7)が使用できるようになります。自作も可能ですが、標準的な関数型インターフェースが何種類か用意されています。
- Supplier:引数なしで値を返すインターフェイス。(get, ()→T)
- Consumer:引数ありで処理だけを行い値を戻さないインターフェイス。(accept, T→void)
- Predicate:引数ありで条件判定処理を行い結果をboolean型で戻すインターフェイス。(test, T→boolean)
- Function:引数ありで処理を行い任意の型のオブジェクトを戻すインターフェイス。(apply, T→R)
- BiFunction:引数(2個)ありで処理を行い任意の型のオブジェクトを戻すインターフェイス。(apply, T,U→R)
- ToIntFunction:引数ありで処理を行いint型の値を戻すインターフェイス。(applyAsInt, T→int)
- ToLongFunction:引数ありで処理を行いlong型の値を戻すインターフェイス。(applyAsLong, T→Long)
- ToDoubleFunction:引数ありで処理を行いDouble型の値を戻すインターフェイス。(applyAsDouble, T→Double)
():主なメソッド名, 引数の型→戻り値の型
以下は自作関数型インターフェイスの単純な定義・使用サンプルです。
[InterfaceExample関連コード説明]
・趣味名を指定し、「my hobby : 趣味名 」を出力するプログラムです。
・009~012で抽象メソッドgetHobby(戻り値:文字列)を持つ関数型インターフェイスIHobby定義しています。
・003~005でIHobbyの処理実装をラムダ式を用いて定義しています。
・006でIHobbyのgetHobbyメソッドを実行しています。
001 public class InterfaceExample{
002 public static void main(String[] args){
003 IHobby hb = (String hobby) -> {
004 return "my hobby : " + hobby;
005 };
006 System.out.println(hb.getHobby("cycling"));
007 }
008 }
009 @FunctionalInterface
010 interface IHobby{
011 public String getHobby(String hobby);
012 }
以下はFunction標準関数型インターフェイスを使用した単純なサンプルです。
[LambdaParameter関連コード説明]
・指定した値を5倍して出力するプログラムです。
・016~018で値を5倍するメソッドgetFiveTimes(戻り値:Integer型)を定義しています。
・007~009でexecuteメソッドを実行して5倍値を取得しています。
・012で007~009で定義した第2引数のラムダ式を関数型インターフェイスで受けています。
・013で関数型インターフェイスのapplyメソッドを実行しています。
001 package functiontest;
002 import java.io.IOException;
003 import java.util.function.Function;
004 public final class LambdaParameter {
005 public static void main(String[] args) throws IOException {
006 Integer value = 1;
007 Integer res = execute(value, t -> {
008 return getFiveTimes(t);
009 });
010 System.out.println("結果:" + res);
011 }
/**
* 引数の関数インターフェイス(ラムダ式)を実行するメソッド
*/
012 public static <R> R execute(Integer value, Function<Integer, R> fnc) {
013 R rtnval = fnc.apply(value);
014 return rtnval;
015 }
/**
* 具体的な処理を実行するメソッド(値を5倍する)
*/
016 public static Integer getFiveTimes(Integer value) {
017 return value * 5;
018 }
019 }
・Map
既存のMapクラスにも新しいメソッドが追加されています。
追加された主なメソッドは、forEach、replaceAll、computeIfAbsent、computeIfPresent、compute、mergeとなっています。
- forEach:Map全要素について順次指定した処理を行います。
- replaceAll:Map全要素の値を指定関数の値に全て置き換えます。
- computeIfAbsent:指定キーが無い場合に、指定関数の値を持つMap要素を追加します。
- computeIfPresent:指定キーが有る場合に、Map要素の値を指定関数の値に設定します。
- compute:指定キーが無い場合にはMap要素(key,指定関数値)が追加され、有る場合は指定関数の値に変更します。
- merge:指定キーが無い場合にはMap要素(key,引数値)が追加され、有る場合は指定関数の値に変更します。
以下は単純なサンプルです。(k:Mapキー v:Map値 p:引数値 key:指定キー)
[Map関連コード説明]
mapの型はString,Stringとしています。
・forEach(001):
a. mapの全要素について[キー:値]の形式で出力します。
・replaceAll(002~):
a. map全要素の値をヌル→[キー]に、ヌル以外→[キー+値の先頭2文字]に置き換えます。
・computeIfAbsent(008~):
a. keyがmapのキーに存在する場合
値は更新されず、値が戻り値となります。
b. キーに存在して値がヌルの場合
値が[キー+"-addition"]に更新され、その値が戻り値となります。
c. キーに存在しない場合
キーがkeyで、値が[key+"-addition"]の値をmapに追加し、その値が戻り値となります。
・computeIfPresent(011~):
a. keyがmapのキーに存在する場合
値が[キー+値+"-addition"]に更新され、その値が戻り値となります。
b. キーに存在して値がヌルの場合
値は更新されず、ヌルが戻り値となります。
c. キーに存在しない場合
値は追加・更新されず、ヌルが戻り値となります。
・compute(014~):
a. keyがmapのキーに存在する場合
値が[キー+値+"-addition"]に更新され、その値が戻り値となります。
b. キーに存在して値がヌルの場合
値が[キー+値(ヌル)+"-addition"]に更新され、その値が戻り値となります。
c. キーに存在しない場合
(key, [key+値(ヌル)+"-addition"])の要素をmapに追加、その値が戻り値となります。
・merge(017~):
a. keyがmapのキーに存在する場合
値が[値+"-add"]に更新され、その値が戻り値となります。
b. キーに存在して値がヌルの場合
値が["-add"]に更新され(ヌル値は無視)、その値が戻り値となります。
c. キーに存在しない場合
(key, ["-add"])の要素(ヌル値は無視)をmapに追加、その値が戻り値となります。
forEach:
001 map.forEach((k, v) -> System.out.println(k + ":" + v));
replaceAll:
002 map.replaceAll((k, v) -> {
003 if (null == v) {
004 return k;
005 }
006 return k + v.substring(0, 2);
007 });
computeIfAbsent:
008 map.computeIfAbsent(key, k -> {
009 return k + "-addition";
010 });
computeIfPresent:
011 map.computeIfPresent(key, (k, v) -> {
012 return k + v + "-addition";
013 });
compute:
014 map.compute(key, (k, v) -> {
015 return k + v + "-addition";
016 });
merge:
017 map.merge(key, "-add", (v, p) -> v + p);
・List
既存のListクラスにも新しいメソッドが追加されています。
追加された主なメソッドは、forEach、removeIf、replaceAll、stream、parallelStreamとなっています。
- forEach:全要素について順次指定した処理を行います。
- removeIf:指定した関数の条件に合致した要素を削除します。
- replaceAll:指定要素の値を指定関数の値に全て置き換えます。
- sort:指定要素の値をソートします。
- stream:Stream(直列ストリーム)を返します。
- parallelStream:Stream(並列ストリーム:並列に処理可能なStream)を返します。
以下は単純なサンプルです。(v:リスト値 value:外部値)
[List関連コード説明]
listの型はStringとしています。
・forEach(001):
a. listの全要素について値を出力します。
・removeIf(002~):
a. list内に指定値が有る:該当要素は削除され、trueが戻ります。
b. 指定値がヌル:該当要素は削除されずfalseが戻ります。
c. 指定値が無い:要素は削除されずfalseが戻ります。
・replaceAll(008~):
a. 全要素について値がヌル以外:値の先頭2文字に更新されます。
b. 全要素について値がヌル:値は更新されません。
・sort(014):
a. 全要素に関して自然順・null最大値ソートを行います。
・stream(015):
a. listのStreamを取得します。
・parallelStream(016):
a. listの並列Streamを取得します。
forEach:
001 list.forEach(v -> System.out.println(v));
removeIf:
002 list.removeIf(v -> {
003 if (null == v) {
004 return false;
005 }
006 return v.equals(value);
007 });
replaceAll:
008 list.replaceAll(v -> {
009 if (null == v) {
010 return v;
011 }
012 return v.substring(0, 2);
013 });
sort:
014 list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
stream:
015 Stream sm = list.stream();
parallelStream:
016 Stream sm = list.parallelStream();
・StreamAPI
配列やCollectionなどを扱うためのAPIで、値の集計などのデータ処理に利用できます。Streamのメソッドは、動作内容で中間処理用と終端処理用の2種類に分類されます。
0個以上の中間処理用メソッドを経由して終端処理用メソッドを実行します。
プリミティブなStreamAPIも用意されています。
中間処理用メソッド
asDoubleStream、asLongStream、boxed、distinct、filter、flatMap、flatMapToDouble、flatMapToInt、flatMapToLong、limit、map、mapToDouble、mapToInt、mapToLong、mapToObj、onClose、parallel、peek、sequential、skip、sorted、unordered
以下は代表的メソッドの簡易説明です。
- filter:条件に一致したオブジェクトを抽出します。
- map:入力値を型変換した値にしてStream型で返します。
- flatMap:入力値を複数の型変換した値にして処理しStream型で戻します。
- distinct:重複した値を排除します。
- sorted:ソートします。
- limit:指定した個数に限定します。
以下は単純なサンプルです。
(stream:Streamインスタンス v:ストリーム値 list:外部List numlimit:外部値)
[Stream関連コード(中間処理)説明]
streamの型はIntegerとしています。
・filter(001~):
a. stream全要素について3の倍数を抽出し、Streamで戻しています。
・map(004):
a. listに文字列ファイル名が格納されており、これをlist→streamに変換します。
b. その後更にStreamに変換後、この値を戻しています。
・flatMap(005~):
処理内容:list各要素について数字分だけ先頭に"abc"をつけたコピーを作成し戻しています。
a. listに半角1文字+数字1桁が格納されており、数字の個数分["abc"+値]の配列を作成します。
b. その後この配列をStream型に変換します。更にこの値をListに変換しています。
・distinct(010):
a. stream全要素について重複要素を排除して同型で戻しています。
・sorted(011):
a. streamの全要素数を昇順でソートし同型で戻しています。
・limit(012):
a. streamの全要素数をnumlimitに制限し同型で戻しています。
filter:
001 stream.filter(v -> {
002 return (v%3) == 0;
003 });
map:
004 list.stream().map(Paths::get);
flatMap:
005 list.stream().flatMap(v -> {
006 String[] array = new String[Integer.parseInt(v.substring(1))];
007 Arrays.fill(array, "abc" + v);
008 return Stream.of(array);
009 }).collect(Collectors.toList());
distinct:
010 stream.distinct();
sorted:
011 stream.sorted();
limit:
012 stream.limit(numlimit);
終端処理用メソッド
allMatch、anyMatch、average、collect、count、findAny、findFirst、forEach、forEachOrdered、iterator、max、min、noneMatch、reduce、spliterator、sum、summaryStatistics、toArray
以下代表的メソッドの簡易説明です。
- allMatch:指定関数の条件に全て合致すればtrueを返します。
- anyMatch:指定関数の条件に合致する値があればtrueを返します。
- collect:要素の値を使用して結果を作成します。
- count:要素の数を返します。
- reduce:要素の値を集約します。
- max:最小値(Optional型)を返します。
- min:最小値(Optional型)を返します。
- toArray:配列に変換します。
以下は単純なサンプルです。
(stream:Streamインスタンス v:ストリーム値 array:外部配列 ac:アキュムレータ)
[Stream関連コード(終端処理)説明]
・allMatch(001~):
a. streamを全一致条件(0<値>10)でチェックしています。全要素が一致するとtrueが戻ります。
・anyMatch(007~):
a. streamをany条件(=5)でチェックしています。:どれか1つ以上一致するとtrueが戻ります。
・collect(010~):
a. 例1:arrayをstreamに変換し、CollectorsクラスのtoListメソッドを使用してList型を戻しています。Collectorsクラスにはstream→他クラスへ変換するためのメソッドが複数用意されています。
b. 例2:listをstreamに変換し、stream値をStringBuilderに格納して同型を戻しています。(第3引数の実質的な意味合いはこのケースの場合ありません。通常並列処理の場合意味合いを持ちます。)
collectメソッドの引数説明は以下の通りです。
* 第1引数:結果を格納するオブジェクト作成関数。
* 第2引数:Stream値をオブジェクトに入れる関数。
* 第3引数:結果オブジェクト同士を結合する関数。
・count(012):
a. streamの要素数を取得しています。long型で戻ります。
・reduce(013):
a. streamの要素を集約しています。Optional型で戻ります。streamがa,b,cという3要素の場合abcが戻ります。
・max(014):
a. streamの最大値を自然順で取得しています。
・min(015):
a. streamの最小値を自然順で取得しています。
・toArray(016):
a. streamを配列型に変換しarrayに格納しています。
・filter+forEach(017~):
a. stream全要素について3の倍数を抽出し標準出力しています。
allMatch:
001 stream.allMatch(v -> {
002 if ((0 < v) && (10 > v)) {
003 return true;
004 }
005 return false;
006 });
anyMatch:
007 stream.anyMatch(v -> {
008 return v.equals(5);
009 });
collect:(複数タイプ存在)
010 //例1:Arrays.stream(array).collect(Collectors.toList());
011 //例2:list.stream().collect(StringBuilder::new, (b, v) -> b.append(v), (b1, b2) -> b1.append(b2));
//第1引数:結果収納オブジェクト生成、第2引数:ストリーム値を収納 第3引数:オブジェクト結合
count:
012 list.stream().count();
reduce:(複数タイプ存在)
013 list.stream().reduce((ac, v) -> ac + v);
max:
014 list.stream().max(Comparator.naturalOrder());
min:
015 list.stream().min(Comparator.naturalOrder());
toArray:
016 Object[] array = list.stream().toArray();
filter+forEach:
017 stream.filter(v -> {
018 return v%3 == 0;
019 }).forEach(System.out::println);
・Optional
Optionalは値を1つラップするクラスです。
ヌルや値の有無をチェックすることができます。
Optional.ofNullable、Optional.empty()、opt.isPresent()、・・
Java8での関数型プログラミングの例
ネットや書籍では数行のサンプルが多いのですが、ここでは一定の機能を持ったレベルの例を提示することにしました。
ただし、関数プログラミングはこうあるべきといった内容ではなく、単に初歩的理解のための参考コーディングとしての視点で作成しています。
機能は商品の在庫管理でメインクラスとFunctionStockクラスの2クラスから構成されています。
FunctionStockクラスは形式的に出来るだけ多くのAPI使用例を提示するため必要としない箇所でも関数型プログラミングを行っていますので、予め了承お願いいたします。
クラス概要
StockMain:在庫管理のメインクラス。
関数型プログラミングは行っていません。
FunctionStock:実際に在庫処理を行うクラス。
複数のメソッドで関数型プログラミングを行っています。
IStock:在庫処理クラスのインターフェイス。
IStockExecuter:在庫処理の自作関数インターフェイス。
詳細な機能はメインクラスのコメントに、メソッドの概要もコメントを付けて記述していますので参考にして下さい。
☆印で使用している関数型プログラミングのタイプを記述しています。
[StockMain関連コード説明]
013~014で在庫数及び付属品構成を格納するMapを定義しています。
025~036で6個の品目及び初期在庫数の設定をしています。cdとmagazineには付属品があります。
016~022で具体的な処理を設定しています。
bookは2つの仕入れ処理、1つの販売処理を定義しています。
magazineは1つの販売処理を定義しています。
最後で在庫一覧出力処理を定義しています。
037~046はstockメソッドで仕入れ処理を実行しています。
047~056はsaleメソッドで販売処理を実行しています。数量は負値としています。
057~066はgetメソッドで指定商品の在庫数を出力しています。
067~076はgetListメソッドで商品の在庫一覧を出力しています。
仕入れ処理、販売処理、在庫数出力、在庫一覧出力はいずれもFunctionStockのexecuteメソッドで実行しています。
001 package stocksample;
002 import java.util.Arrays;
003 import java.util.List;
004 import java.util.Map;
005 import java.util.TreeMap;
/**
* 在庫管理のメインクラス
* 機能:仕入れ処理(stock)、販売処理(sale)、在庫確認処理(get)、在庫一覧出力処理(getList)
* ():メソッド名
* 在庫データ処理形式:メソッド名(商品名, 数量)
* 各商品には付属品(複数可能ただし各数量は1個限定)が付けられるようになっていてその付属品の在庫管理も行っている。
* 仕入れ処理:指定商品の在庫数を仕入れ数だけ増やす。指定商品が登録されていない場合は商品登録も行う。
* 販売処理:指定商品の在庫数を販売数だけ減らす。ただし付属品も含めて在庫がない場合は処理を中止する。
* 在庫確認処理:指定商品の在庫数を出力する。
* 在庫一覧出力処理:全商品の在庫数を出力する。付属品リストも合わせて出力する。
* エラーの扱い:発生時点でエラーメッセージを出力し以降の処理を中止する。
*/
006 public class StockMain {
007 private static Map<String, Integer> stockMap;
008 private static Map<String, List<String>> subStockMap;
009 private static IStock stockObject;
010 private static final boolean funcOption = true; //JDK8版
//private static final boolean funcOption = false; //JDK6,JDK7版
011 public static void main(String[] args) {
012 System.out.println("**start**");
//
//初期マップ値をセット
013 stockMap = new TreeMap<String, Integer>();
014 subStockMap = new TreeMap<String, List<String>>();
015 setInitialMap();
//
//在庫データ処理
016 stock("book", 1);
017 stock("book", 2);
018 sale("book", 2);
019 get("book");
020 sale("magazine", 2);
021 get("magazine");
022 getList();
//
023 System.out.println("**end**");
024 }
/**
* 初期マップ値をセットするメソッド
*/
025 private static void setInitialMap() {
026 List<String> cdlist = Arrays.asList("posterA", "posterB", "posterC");
027 subStockMap.put("cd", cdlist);
028 List<String> mglist = Arrays.asList("bagA");
029 subStockMap.put("magazine", mglist);
030 stockMap.put("cd", 3);
031 stockMap.put("magazine", 3);
032 stockMap.put("posterA", 3);
033 stockMap.put("posterB", 3);
034 stockMap.put("posterC", 3);
035 stockMap.put("bagA", 3);
036 }
/**
* 仕入れ処理を行うメソッド
*/
037 private static void stock(String productName, int quantity) {
038 if (funcOption) {
039 stockObject = new FunctionStock(productName, quantity, "add");
040 } else {
041 stockObject = new Stock(productName, quantity, "add");
042 }
043 setMap();
044 int result = stockObject.execute();
045 if (0 > result) System.exit(result);
046 }
/**
* 販売処理を行うメソッド
*/
047 private static void sale(String productName, int quantity) {
048 if (funcOption) {
049 stockObject = new FunctionStock(productName, -quantity, "add");
050 } else {
051 stockObject = new Stock(productName, -quantity, "add");
052 }
053 setMap();
054 int result = stockObject.execute();
055 if (0 > result) System.exit(result);
056 }
/**
* 指定商品の在庫数を出力するメソッド
*/
057 private static void get(String productName) {
058 if (funcOption) {
059 stockObject = new FunctionStock(productName, "get");
060 } else {
061 stockObject = new Stock(productName, "get");
062 }
063 setMap();
064 int result = stockObject.execute();
065 if (0 > result) System.exit(result);
066 }
/**
* 在庫一覧を出力するメソッド
*/
067 private static void getList() {
068 if (funcOption) {
069 stockObject = new FunctionStock("getlist");
070 } else {
071 stockObject = new Stock("getlist");
072 }
073 setMap();
074 int result = stockObject.execute();
075 if (0 > result) System.exit(result);
076 }
/**
* ストックオブジェクトにマップをセットするメソッド
*/
077 private static void setMap() {
078 stockObject.setStockMap(stockMap);
079 stockObject.setSubStockMap(subStockMap);
080 }
081 }
[FunctionStock関連コード説明]
028~042はコンストラクタで013~014でproductName, quantity, typeを設定しています。
typeは処理の種類を表しadd、delname、get、getlistを指定できます。
・executeメソッド
044で指定在庫データを確認用出力をしています。
045~048で指定在庫データをチェックしています。getDataCheckFunction().get()メソッドで在庫データのチェックを行い、結果をStringを取得後、Optional.ofNullableでOptionalに変換し、ifPresentでOptionalのnull判定を行いエラーがあればoutputErrorMessageでエラー表示しています。
052で在庫データ処理をexecuteStock().execute()メソッド(IStockExecuter自作関数インターフェイス)で実行し、処理結果をOptional 総称型:Integerに格納しています。
053で在庫データ処理でのエラーメッセージ生成をgetErrorKeyFunction().apply(result)メソッドで行い、outputErrorMessageで出力しています。
・getDataCheckFunctionメソッド
066~078で在庫データをチェック(ヌルチェック等)する関数型インターフェイス(Supplier 総称型:String)の実装を定義しています。
・executeStockメソッド
079~096で在庫データを処理(処理タイプ:add,delname,get,getlist)する関数型インターフェイス(IStockExecuterの)実装を定義しています。
updateStock().get()、 deleteStockName().get()、getStock().get()、outputStockList(getStockList().get())、outputSubStockList()を呼び出しています。
・updateStockメソッド
097~126で在庫数を更新する関数型インターフェイス(Supplier 総称型Optional)の実装を定義しています。
addToStockMap().apply(・・)メソッドを使用して在庫数を更新処理しています。
・addToStockMapメソッド
127~138で在庫数を具体的に更新する関数型インターフェイス(BiFunction 総称型:String, Integer, Optional)の実装を定義しています。
Mapのcomputeメソッドを使用して在庫数の増減算術を行っています。
・deleteStockNameメソッド
139~147で在庫対象品目(在庫データ)を削除する関数型インターフェイス(Supplier 総称型:Optional)の実装を定義しています。
Mapのremoveメソッドを使用して在庫対象品目の削除処理を行っています。
・getStockメソッド
148~154で特定品目の在庫数を取得する関数型インターフェイス(Supplier 総称型:Optional)の実装を定義しています。
MapのgetOrDefaultメソッドを使用して在庫数取得を行っています。
・getStockListメソッド
155~166で在庫数一覧を取得する関数型インターフェイス(Supplier 総称型:String)の実装を定義しています。
MapのforEachメソッドを使用して在庫一覧を生成しています。
・getErrorKeyFunctionメソッド
167~175で在庫処理結果をチェックする関数型インターフェイス(Function 総称型:Optional, String)の実装を定義しています。
Optional(errindex)のmapメソッドでエラーの場合のメッセージ生成を定義しています。
・outputSubStockListメソッド
191~204で付属品一覧を出力しています。特定品目に対する付属品出力情報はlist→streamに変換後collectメソッドで生成しています。
・outputErrorMessageメソッド
205~220でmessageKeyに対するエラーメッセージ出力をしています。messageKeyをOptionalに変換後mapメソッドでエラーメッセージを生成しています。
001 package stocksample;
002 import java.util.Arrays;
003 import java.util.List;
004 import java.util.Map;
005 import java.util.Optional;
006 import java.util.function.BiFunction;
007 import java.util.function.Function;
008 import java.util.function.Supplier;
/**
* 在庫管理を行うクラス
*/
009 public final class FunctionStock implements IStock {
010 private String productName;
011 private int quantity;
012 private Map<String, Integer> stockMap;
013 private Map<String, List<String>> subStockMap;
014 private String type;
015 private String errorKey;
016 private String errorProductName;
017 private final List<String> typeList = Arrays.asList("add", "delname", "get");
018 private final List<String> errorKeyList = Arrays.asList("zerostock,subzerostock", "noname", "noname");
019 private final List<String> errorMessageKeyList= Arrays.asList(
020 "nullname", "noname", "number", "zerostock", "subzerostock","keyerror");
021 private final List<String> errorMessageList= Arrays.asList(
022 "★製品名が指定されていません。",
023 "★在庫リストに指定された製品名が存在しません。",
024 "★数量が指定されていません。",
025 "★在庫量がゼロ未満になってしまいます。 <%p1%> <%p2%>個",
026 "★付属品の在庫量がゼロ未満になってしまいます。 <%p1%> <%p2%>個",
027 "★キーが異常です。");
/**
* コンストラクタ
*/
028 public FunctionStock(String productName, int quantity, String type) {
029 this.productName = productName;
030 this.quantity = quantity;
031 this.type = type;
032 };
/**
* コンストラクタ
*/
033 public FunctionStock(String productName, String type) {
034 this.productName = productName;
035 this.quantity = 0;
036 this.type = type;
037 };
/**
* コンストラクタ
*/
038 public FunctionStock(String type) {
039 this.productName = "";
040 this.quantity = 0;
041 this.type = type;
042 };
/**
* 在庫データを処理するメソッド
* ☆OptionalのofNullable、ifPresent及びorElseメソッドを使用
* ☆Functionインターフェイスのapplyメソッドを使用
*/
043 public int execute() {
//在庫データ出力
044 outputData();
//在庫データチェック
045 Optional.ofNullable(getDataCheckFunction().get()).ifPresent(ekey -> {
046 outputErrorMessage(ekey);
047 errorKey = ekey;
048 });
049 if (null != errorKey) {
050 return -1;
051 }
//在庫データ処理
052 Optional<Integer> result = executeStock().execute();
//エラー出力
053 outputErrorMessage(getErrorKeyFunction().apply(result));
054 return result.orElse(-1);
055 }
/**
* 在庫データを出力するメソッド
*/
056 private void outputData() {
057 StringBuilder sb = new StringBuilder();
058 sb.append("処理データ:");
059 sb.append(productName);
060 sb.append(",");
061 sb.append(quantity);
062 sb.append(",");
063 sb.append(type);
064 System.out.println(sb.toString());
065 }
/**
* 在庫データをチェックするメソッド
* ☆関数型インターフェースSupplierを使用
*/
066 private Supplier<String> getDataCheckFunction() {
067 return () -> {
068 if (null == productName || (!"getlist".equals(type) && "".equals(productName))) {
069 return "nullname";
070 }
071 if ("add".equals(type)) {
072 if (0 == quantity) {
073 return "number";
074 }
075 }
076 return null;
077 };
078 }
/**
* 在庫データを処理するメソッド
* ☆自作関数型インターフェースIStockExecuterを使用
* ☆Optionalのemptyメソッドを使用
*/
079 private IStockExecuter executeStock() {
080 return () -> {
081 Optional<Integer> result = Optional.empty();
082 if ("add".equals(type)) {
083 result = updateStock().get();
084 } else if ("delname".equals(type)) {
085 result = deleteStockName().get();
086 } else if ("get".equals(type)) {
087 result = getStock().get();
088 } else if ("getlist".equals(type)) {
089 outputStockList(getStockList().get());
090 outputSubStockList();
091 } else {
092 errorKey = "keyerror";
093 }
094 return result;
095 };
096 }
/**
* 在庫数を更新するメソッド(増減可)
* ☆関数型インターフェースSupplierを使用
* ☆関数型インターフェースBiFunctionのapplyメソッドを使用
* ☆Optionalのofメソッドを使用
* ☆ListのforEachメソッドを使用
* ☆MapのgetOrDefaultメソッドを使用
*/
097 private Supplier<Optional<Integer>> updateStock() {
098 return () -> {
099 if (0 > addToStockMap().apply(productName, quantity).get()) {
100 addToStockMap().apply(productName, -quantity);
101 return Optional.of(-1);
102 }
103 if (0 > quantity) {
104 List<String> slist = subStockMap.get(productName);
105 if (null != slist) {
106 slist.forEach(v -> {
107 if (null != errorProductName) return;
108 int substock = stockMap.getOrDefault(v, -1);
109 if (-1 == substock || 0 > (substock + quantity)) {
110 errorProductName = v;
111 }
112 });
113 if (null == errorProductName) {
114 slist.forEach(v -> {
115 addToStockMap().apply(v, quantity);
116 });
117 }
118 }
119 if (null != errorProductName) {
120 addToStockMap().apply(productName, -quantity);
121 return Optional.of(-2);
122 }
123 }
124 return Optional.of(0);
125 };
126 }
/**
* 商品の在庫数値を更新するメソッド
* ☆関数型インターフェースBiFunctionを使用
* ☆Mapのcomputeメソッドを使用
* ☆Optionalのofメソッドを使用
*/
127 private BiFunction<String, Integer, Optional<Integer>> addToStockMap() {
128 return (pname, qty) -> {
129 int addedValue = stockMap.compute(pname, (k, v) -> {
130 if (null == v) v = 0;
131 return v + qty;
132 });
133 if (0 > addedValue) {
134 return Optional.of(-1);
135 }
136 return Optional.of(addedValue);
137 };
138 }
/**
* 商品の在庫データを削除するメソッド
* ☆関数型インターフェースSupplierを使用
* ☆OptionalのofNullable、isPresent及びofメソッドを使用
*/
139 private Supplier<Optional<Integer>> deleteStockName() {
140 return () -> {
141 int result = -1;
142 if (Optional.ofNullable(stockMap.remove(productName)).isPresent()) {
143 result = 0;
144 }
145 return Optional.of(result);
146 };
147 }
/**
* 在庫数を取得するメソッド
* ☆関数型インターフェースSupplierを使用
* ☆MapのgetOrDefaultメソッドを使用
* ☆Optionalのofメソッドを使用
*/
148 private Supplier<Optional<Integer>> getStock() {
149 return () -> {
150 int result = stockMap.getOrDefault(productName, -1);
151 outputNumberStock(result);
152 return Optional.of(result);
153 };
154 }
/**
* 在庫リストを生成するメソッド
* ☆関数型インターフェースSupplierを使用
* ☆MapのforEachメソッドを使用
*/
155 private Supplier<String> getStockList() {
156 return () -> {
157 StringBuilder sb = new StringBuilder();
158 stockMap.forEach((k, v) -> {
159 sb.append(k);
160 sb.append(":");
161 sb.append(v);
162 sb.append("\n");
163 });
164 return sb.toString().substring(0, sb.toString().length()-1);
165 };
166 }
/**
* 在庫処理結果をチェックするメソッド
* ☆関数型インターフェースFunctionを使用
* ☆Optionalのmap及びorElseメソッドを使用
*/
167 private Function<Optional<Integer>, String> getErrorKeyFunction() {
168 return errindex -> {
169 Optional<String> opkey = errindex.map(eindex -> {
170 if (0 <= eindex) return "";
171 return errorKeyList.get(typeList.indexOf(type)).split(",")[Math.abs(eindex)-1];
172 });
173 return opkey.orElse("");
174 };
175 }
/**
* 在庫数を出力するメソッド
*/
176 private void outputNumberStock(int result) {
177 if (-1 < result) {
178 StringBuilder sb = new StringBuilder();
179 sb.append("☆指定された在庫名の在庫数量:");
180 sb.append(productName);
181 sb.append(" ");
182 sb.append(result);
183 sb.append("個");
184 System.out.println(sb.toString());
185 }
186 }
/**
* 在庫リストを出力するメソッド
*/
187 private void outputStockList(String list) {
188 System.out.println("☆在庫リスト");
189 System.out.println(list);
190 }
/**
* 付属品リストを出力するメソッド
* ☆MapのforEach、getOrDefaultメソッドを使用
* ☆Listのstreamメソッドを使用
* ☆Streamのcollectメソッドを使用
*/
191 private void outputSubStockList() {
192 System.out.println("☆付属品リスト");
193 stockMap.forEach((k, v) -> {
194 List<String> list = subStockMap.getOrDefault(k, null);
195 if (null != list) {
196 StringBuilder sb = list.stream().collect(StringBuilder::new, (ssb, adname) -> {
197 ssb.append(adname);
198 ssb.append(", ");
199 }, (ba, bb) -> {ba.append(bb);});
200 String str = k + " : " + sb.toString();
201 System.out.println(str.substring(0, str.length()-2));
202 }
203 });
204 }
/**
* エラーメッセージを出力するメソッド
* ☆OptionalのofNullable及びmapメソッドを使用
*/
205 private void outputErrorMessage(String messageKey) {
206 if ("".equals(messageKey)) return;
207 Optional<String> mes = Optional.ofNullable(messageKey).map(m -> {
208 String messtr = errorMessageList.get(errorMessageKeyList.indexOf(m));
209 if (-1 < messtr.indexOf("<%p")) {
210 String pname = productName;
211 if (null != errorProductName) {
212 pname = errorProductName;
213 }
214 messtr = messtr.replace("<%p1%>", pname).replace("<%p2%>", String.valueOf(stockMap.get(pname)));
215 }
216 return messtr;
217 });
218 System.out.println(mes.get());
219 System.out.println("★処理は中止されました。");
220 }
221 public void setStockMap(Map<String, Integer> stockMap) {
222 this.stockMap = stockMap;
223 }
224 public void setSubStockMap(Map<String, List<String>> subStockMap) {
225 this.subStockMap = subStockMap;
226 }
227 }
001 package stocksample;
002 import java.util.List;
003 import java.util.Map;
004 public interface IStock {
005 public int execute();
006 public void setStockMap(Map<String, Integer> stockMap);
007 public void setSubStockMap(Map<String, List<String>> subStockMap);
008 }
001 package stocksample;
002 import java.util.Optional;
003 @FunctionalInterface
004 public interface IStockExecuter {
005 public abstract Optional<Integer> execute();
006 }
関数型プログラミング実施のまとめ
- Java8の関数型プログラミング用API使用によりコレクション処理でのループ記述やインデックス記述を無くす事ができることを確認できました。
- とはいえ、どちらかというと実質は表層的な省力化で、これによりJavaプログラミングが大きく変わるという程のものでもないとも思えます。
- もともとJava8の場合オブジェクト指向がベースで関数型プログラミング用APIは付加的なものなので当然と言えば当然という事になります。
- 関数プログラミングを採用する時に気を付けなければいけない点は、APIやメソッドの実装詳細を知っていなければいけないという事です。
APIやメソッドを連結して使用するのが関数プログラミングの基本なので多連結する場合予期せぬ結果となってしまう事が予想されるためです。
特にヌルや異常値入力の扱いは正確に把握しておく必要があります。 - 関数型プログラミング主体にシステム構築を目指すのであればHaskell等の関数型言語での実装を目指すのが適切だと思われます。
- しかし関数型プログラミング主体が向いているシステムが多いという情報は見かけないので個人的にはこれまで通りJavaを基軸にするつもりです。
関数プログラミングに関係する語句の簡易説明
ネットでよく出てくる関数プログラミングに関係する語句の簡易説明を以下に記述しました。
モナドについてはわかりにくいので追加説明を付加しました。
- 関数型インターフェイス:
定義されている抽象メソッドが1つだけあるインターフェースのことでラムダ式と関連します。
基本的には@FunctionalInterfaceアノテーションを付けて使用します。 - ラムダ式:
関数型インターフェースを使用して処理内容を実装・記述するために使用します。
基本構文は(引数) -> {処理}で、引数部分の個数による構文形式は、引数が1個:(str) -> {処理}、引数が2個:(str1, str2) -> {処理}となります。
処理部分の基本形は、{文1;文2;…return 戻り値;}となり、式なので値を戻す事が可能となっています。
Java8での新規メソッドなどではこのラムダ式を引数として渡すことが可能になりシンプルなプログラミングができるようになりました。副作用のない処理部分を記述すれば本来の関数型プログラミングがその部分では実現できることになります。
以前の匿名クラスと類似しています。 - 宣言型プログラミング:
出力を得るための処理手続き(データとアルゴリズム)を記述せずに、目的・性質・制約などを記述して出力を得るプログラミング手法のことです。
純粋関数型言語等いくつかの言語の総称を指す場合もあります。
更に簡単に言えば、何が必要かを設定して、それをどのように処理するかは言語・API・関数などに任せるプログラミング手法のことです。
- 高階関数:
他の関数を引数あるいは結果として返すことができる関数のことです。
Java8ではMap, FlatMap, Filter, Reduce, forEach, anyMatch, allMatchなどがあります。 - StreamAPI:
配列やCollectionなどの集合体を扱い、値の集計やデータを使った処理などが出来るAPIです。
Java8の目玉新機能となっています。 - Optional:
Optionalはnullを含む値をラップし、nullを安全に処理するための関数です。 - メソッド参照:
関数型インターフェースの変数にメソッドそのものを代入することをメソッド参照と言います。
ただし、関数型インターフェースにおける抽象メソッドの引数の個数・型と、代入したいメソッドの引数の個数・型が一致している必要があります。
定義済みのメソッドを引数なしで呼び出せるのがメソッド参照の特徴で、「クラス名::メソッド名」がその基本形となっています。
定義済みのメソッドをラムダ式のように扱うことも可能となっています。
(例)
例1:list.forEach(System.out::print);
例2:Consumer c = System.out::println;
c.accept("abc");
- *副作用:
入力として受けつけたデータ以外の物(変数値など)が変化し(状態変化)、以降における結果に影響を与えることを指します。
代入、インクリメント、デクリメント、入出力などを指します。 - 参照透過性(参照透明性):
同じ入力なら必ず同じ結果を返す性質のこと。 - 遅延評価:
値が必要になるまで計算しないという計算方法で、繰り返し構造を容易に組み込むことが可能となっています。
Java8ではStreamAPIにおいて遅延評価を局所的にサポートしており、StreamAPIでは生成処理指示・中間処理指示では具体的な計算は行われず、終端
処理指示を行ったタイミングで行われるようになっています。また、ラムダ式を使用した遅延処理も可能となっています。
- *モナド:
モナドはプログラム的に言えば関数を結合・合成して実行できるようにするための仕組み・構造・概念です。
一般的に概念を含め細部を理解することは難しいと言われていますが、難しくないという意見もあります。どちらかというと説明書が難しいのではという意見が多いように思われます。
*世の中での定義や説明が必ずしも明確・適格でない語句
[モナドの追加説明]
モナドについての追加説明をプログラムの視点から記述してみます。
「モナド」はいくつかある種類のモナド(モナドインスタンス)の総称で、Haskell(関数型言語)では型クラスになっています。
標準的なモナドとしては以下のものがあります。
Maybeモナド、Listモナド、Identityモナド、Eitherモナド、Stateモナド、IOモナド、Writerモナド、Readerモナド、・・
(HaskellではMaybe型、List型、・・・・とも呼びます。)
モナドとなるためには、以下の3つの条件を満足する必要があります。
この条件を満たせばモナドと呼ぶことができます。
(1)1つの型引数を受け取ること。
(2)returnとbind(>>=演算子)で操作できること。
returnはモナドに値を入れるための関数です。
bindはモナドの値を関数に渡すための演算子で、モナド >> 関数のように記述して戻り値はモナドに入れます。
(3)定められているモナド則(ルール)を満たすこと。
条件を満たせばモナドとなるので以下の手順で自作することも可能です。
(1)モナドとする型を定義します。
(2)Monadのインスタンスを定義します。
(returnやbindの実装が含まれます。)
このモナドの主目的は、
モナド値 >>= モナド型関数1 >>= モナド型関数2 >>= モナド型関数3 ---> モナド値を得る
のように連続して関数を実行できるようにすることです。
またモナドでは結合する時に何らかの処理を入れることも可能となっています。
モナド値:モナドが持つ値のことです。
モナド型関数:モナド値を受け取り処理結果をモナド値で戻す関数のことです。
モナドは具体的には以下のようにして使用できます。(単なる一例です。言語はHaskellです。)
-
let addOne x = Just(x + 1) --関数addOneの定義(JustはMaybeモナドの値である事を表現しています。)
-
let addTwo x = Just(x + 2) --関数addTwoの定義
-
return 2 >>= addOne >>= addTwo --Maybeモナドに2を入れて関数addOneとaddTwoを実行
結果:Just 5
これはMaybeモナドを使用して2つの関数を結合している例で、>>=は結合するための演算子です。
最後のreturn 2 >>= addOne >>= addTwoは以下のような形式でも指定することができます。
do return 2
addOne
addTwo
モナドを更に理解するためには以下の手順がよいかと思います。
- まずネットで簡単検索し全体を把握(深入りはしないで表層だけにする)。
- Haskell言語の簡易習得(説明がHaskell言語を使用するものが多いため)。
- Haskellインストール。
- Haskellでモナドを使ってみる。
- Haskellでモナドを作ってみる。
- モナドの説明書を読んでみる。
ちなみに私はHaskellを使用した事はありません。
<Monad定義>
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
fail :: String -> m a
fail msg = error msg
最後に
以上です。最後までお読み頂き有難うございました。
「実際Javaシステム開発においてどの程度関数型プログラミングを取り込むべきなのか?」を中心テーマとして第2部を投稿しました。ぜひご覧ください。
第2部へのリンク
「分かりにくいmap,flatMap,collectの追加説明と知っておくと便利なCollector,Collections,Arrays」をテーマとして第3部を投稿しました。
ぜひご覧下さい。
第3部へのリンク