【ラムダ式を学ぶ】第1回 中学生にも分かる! 関数型プログラミングとは?
前回の記事でお話しました通り、今日からJavaのラムダ式を理解するための記事を連載します。
関数型とはそもそも何なのか、ラムダ式とは何なのか、どういったタイミングで使うのかなどを段階的に解説していきます。
今回は第1回ということで、「関数型プログラミングとは何か」、そして「オブジェクト指向型プログラミングとは何が違うのか」を滅茶苦茶分かりやすくして解説していきたいと思います。
✩目次
1.関数型プログラミングとは何かを中学1年生にも分かるような例で説明するよ
変数ってあるじゃないですか。xとかyとか。
例えば、
1-1.
2 + 3 = 5
という式がありますよね。
この3の部分をxに、5の部分をy置き換えると
1-2.
2 + x = y
という式になります。
この時、xに3を入れると、当然ながらyは5になりますね。
ではxに4を入れたらどうなりますか?
1-3.
2 + x = y x <- 4 ※xに4を入れる 2 + 4 = y y = 6
yは6になりましたね。
xに入れる値によって、計算結果であるyの値が変わります。
このxに入れる値によってyの値が決まることを、
『yはxの関数である』
と言います。
出てきました、関数という言葉。
上記の例では関数xには3や4などの数値を渡してきましたが、関数を渡すこともできます。
以下の関数をご覧ください。
1-4.
y = x + 2 ・・・① x = -y + 6 ・・・②
①の関数に②の関数を渡してみましょう(数学では代入と習ったハズ)
1-5.
y = (-y + 6) + 2 2y = 8 y = 4
①の関数に②の関数を入れた結果、yの値が求まりましたね。
大分遠回りな説明から始まりましたが、1-5の例こそが関数型プログラミングで実現できることなんです。
つまり、関数(①)に関数(②)を渡した結果(yの値)を取得できるようになるプログラミング手法、それが関数型プログラミングです。
(※ここではそう結論付けますが、実際に決まった定義があるわけではありません)
2.オブジェクト指向型と関数型の違い
1)背景の違い
結論から言うと、2つは全くの別の用途を目指して作られた技法です。
派閥が違うとか、関数型言語がオブジェクト指向型言語の上位互換的に作られたものであるとか、そういうことではありません。
それぞれが、それぞれの用途のために作られました。
これからその2つの生まれた背景をWikipediaの情報をまとめて説明します。
まずはオブジェクト指向の背景から。
(~背景~)
計算機(PC)の発展によって大規模なシステムが構築できるようになってきたけど、そのシステムの動きを上から下まで手続き的に書いていたら超長いコードになるし超分かりにくいから開発が超大変だよね。
だから同じ定義と処理を持つものはモジュールという単位でまとめてしまって、ついでに管理しやすくなるような設計をしたものをオブジェクトと呼んで、再利用性の高いコードにしよう!! ついでにオブジェクトには人が親しみやすい名前をつけて、そのオブジェクトの振る舞いが一発で分かるようにしようね!! そうすればきっとコミュニケーションが捗るハズ・・・
なげーよっ!!!
ごめんなさい、長かった。
つまり、 長いコードを短く分かりやすくして開発コストを下げようという目的のため生まれたのがオブジェクト指向型言語です。
次に関数型の背景を。
(~背景~)
プログラミングをより数学的記法に近づけよう!
・・・以上!!
・・・・・・え? 終わり?
終わりです(スマヌ)。
なんで数学的記法に近づけたかったの? という説明はありませんでした。
その他は調べようと思ってもEnglishな記事しか出てこなかったのです。
ただ、数学的な記法は馴染みが無い人にとっては分かりにくいですが、無駄を極限まで削って書かれるので簡潔であるという特徴があります。
プログラミング言語を高度で効率的な計算に用いたい、という考えや、純粋により短くより美しくを追求したかった、のではないかなと私は推測しました。
2)特徴の違い
早速、オブジェクト指向から。
『名前』という定義と、『意気込む』という処理を持っている『社会人』というオブジェクトがあるとします。
2-1.
public class 社会人{ String 名前 = "涼風青葉"; public void 意気込む(String セリフ) { if ("涼風青葉".equals(名前)) { System.out.println(セリフ); } else { System.out.println("・・・・・・・・・・"); } } }
そして『社会人』オブジェクトを利用した以下のような処理があるとします。
2-2.
private static void method1 (社会人 obj) { obj.意気込む("「今日も1日がんばるぞいっ!」"); // ① }
この処理を通った時、①で標準出力した時の結果はどうなるでしょうか。
「今日も1日がんばるぞいっ!」って出力されますかね・・・?
正解は『分からない』なんですよねぇ・・・
【引用:涼風青葉 - HIDE | Illustrations - MediBang】
確かに、この社会人オブジェクトの『名前』という項目には「涼風青葉」という初期値が入っています。しかしもしかすると、この社会人オブジェクトのインスタンスが生成された後に『名前』には「滝本ひふみ」という値が設定されているかもしれません。
つまり、そのオブジェクトの設定値に依存するわけです。
また、以下の例を見てみましょう。
オブジェクトは同時に異なる2つのインスタンスを持つことができる、ということを説明するための例です。
2-3
public static void main(String[] args){ 社会人 obj1 = new 社会人(); obj1.名前 = "涼風青葉"; obj1.意気込む("「今日も1日がんばるぞいっ!」"); // ① 社会人 obj2 = new 社会人(); obj2.名前 = "滝本ひふみ"; obj2.意気込む("「今日も1日がんばるぞいっ!」"); // ② }
上記の処理を通った際に出力される内容は以下の通りです。
①の時:「今日も1日がんばるぞいっ!」
②の時:「・・・・・・・・・・」
①と②では、同じ社会人オブジェクトの同じメソッドを全く同じ引数で呼び出しているのに実行結果が異なりますね。
何故なら①と②では異なった状態のインスタンスを呼んでいるからです。
①では涼風青葉という状態の社会人オブジェクトのインスタンスを、②では滝本ひふみという状態の社会人オブジェクトのインスタンスを使用して『意気込む』という処理を呼んでいたのです。
このように、オブジェクト指向型プログラミングには、処理の実行結果がオブジェクトのインスタンス状態に左右されるという特徴があります。
次に、関数型を。
以下のような『意気込む』という処理があったとします。
2-4.
private static void 意気込む(String セリフ) { System.out.println(セリフ); }
この時、引数『セリフ』に『"「今日も1日がんばるぞいっ!」"』を渡してあげると実行結果は、
「"今日も1日がんばるぞいっ!"」
となります。
また、引数『セリフ』に『"「・・・・・・・・・・」"』を渡してあげると実行結果は、
「・・・・・・・・・・」
となります。
それでは以下のように処理を呼び出してみます。
2-5.
public static void main(String[] args){ 意気込む("「今日も1日がんばるぞいっ!」"); // ① 意気込む("「・・・・・・・・・・」"); // ② 意気込む("「今日も1日がんばるぞいっ!」"); // ③ } private static void 意気込む(String セリフ) { System.out.println(セリフ); }
上記の処理を通った際に出力される内容は以下の通りです。
①の時:「今日も1日がんばるぞいっ!」
②の時:「・・・・・・・・・・」
③の時:「今日も1日がんばるぞいっ!」
当然の事かと思われるでしょうが、①と③で同じ引数を渡した結果は同じになります。
このように、関数型プログラミングには外の情報に実行結果を左右されず、同じ引数を渡せば必ず同じ結果が返ってくるという特徴があります。
結局何が違うの? を説明すると、
オブジェクト指向型はそのオブジェクト自体に状態という独自性を持たせることで、同じ処理から別の結果を生み出すことができる。
一方で関数型はいつどこから呼ばれても全く同じ結果を返すことができる。
上記のような違いがあります。
3.簡単なラムダ式
お待たせいたしました。ようやくラムダ式が登場します。
ただ、まだこの例は実用性が皆無です。
それを念頭に、「ラムダ式ってこういう形で書くんだ」ということを知って頂ければと思います。
まずは早速サンプルコードから見てください↓
3-1. public class SampleRamdaTest { public static void main(String[] args) { CalcIntf plus = (n1,n2) -> n1 + n2; // 足し算用の関数が入った変数だよ CalcIntf minus = (n1,n2) -> n1 - n2; // 引き算用の関数が入った変数だよ CalcIntf multi = (n1,n2) -> n1 * n2; // 掛け算用の関数が入った変数だよ calc(plus); // 足し算用の関数を引数にしたよ calc(minus); // 引き算用の関数を引数にしたよ calc(multi); // 掛け算用の関数を引数にしたよ } /** * 5と2という数字を使って何らかの計算がしたいメソッド */ private static void calc(CalcIntf func) { int y = func.calculate(5,2); System.out.println("計算結果は" + y + "だよ"); } /** * 計算インターフェース */ @FunctionalInterface public interface CalcIntf { public int calculate(int n1,int n2); } }
処理結果は以下です。
計算結果は7だよ 計算結果は3だよ 計算結果は10だよ
わかりませんね?
わからないでいいです、以下の説明の後に何となくわかってれば。
それでは解説をしていきます。
上記の例で注目して欲しいポイントは以下です。
① 計算インターフェース
② 「(n1, n2) -> n1 + n2」という変な式。
③ 値ではなくて関数(n1 + n2など)が引数になっている。
① 計算インターフェース
Javaのインターフェースについてはここで説明しません。
詳しくは他サイト様をご覧ください。
この記事においてはインタフェースの「@FunctionalInterface」に注目です。
このアノテーションを付けたインターフェース内では、2つ以上のメソッドを定義することができなくなります。
つまりですね、ラムダ式を使うためだけのインターフェースだよ宣言なんですね。
2つ以上定義しようとするとコンパイラに怒られます。
なんで1つだけなの?
理由は以下②で説明します。
② 「(n1, n2) -> n1 + n2」という変な式
色分けして、日本語とソースコードの対応を下に示します。
CalcIntf plus = (n1, n2) -> n1 + n2 ;
計算インターフェースのcalculateメソッドは、自身の引数n1, n2を使ってn1 + n2をする。
「calculateメソッドだけソースコードに対応してなくない?」
と思いましたよね?
実はこれが①で言っていた、ラムダ式を使うためのインターフェース内にメソッドは1つしか定義してはいけません、という制限の理由です。
「計算インターフェース内にメソッドが「calculate」1つしかないんだから、ラムダ式内で使われるメソッドは「calculate」だよね」
と勝手にコンパイラが自動的に判断してくれるんです。
だから、コンパイラの自動判断を邪魔しないように、2つ以上メソッドは定義しちゃダメなんですよ、ということだったんですね。
③ 値ではなくて関数(n1 + n2など)が引数になっている
関数(n1 + n2という式)を関数(calcメソッド)の引数として渡し、そしてcalcメソッド内でその式を利用して計算ができています。
冒頭で述べた通り、関数に関数を代入して結果を求める事ができていますよね?
ラムダ式の基本的な部分の解説は終了です。
次回からは実用的な例を見ていきたいと思います!
4.おわりに
さて、オブジェクト指向型と関数型の違いを理解することはできたでしょうか。
今回は滅茶苦茶簡単に説明することに主眼を置きました。
だから説明の足りていない部分はいっぱいあります。特に最後のラムダ式とかは。
でも、Javaプログラマーや、その他オブジェクト指向型言語プログラマーにとって、今まで壁一枚隔てて考えられていた関数型という考え方を少しでも身近に感じていただくことができたなら幸いです。
次回以降は、多少実践的なラムダ式の使い方を見ていこうと考えています。
何か分かりにくかった説明部分などあればコメントいただければ幸いです。
あと、この記事でオブジェクト指向型と関数型の違いが理解できたよ! って方は「☆いいね」つけていただければ嬉しいです。明日の勉強の励みとなります。
それでは今回は以上となります。
ここまでご精読いただきありがとうございました。