オブジェクト指向プログラミングObject Oriented Programming (以下OOP), 関数型プログラミングFunctional Programming (FP)のコンセプトを紹介します.どっちがいいって訳はなく、どっちも超頭いい先人たちが思いついたアイデアなので知っていて損は無いし、何なら両方使っちゃえばって感じです.
考え方の紹介なのでコードは(あんまり)無いです.長くなってしまったので今回は関数型プログラミングを主に扱います.どうせオブジェクト指向はいろいろ記事出てるし.
なんでこんなこと考えるのさ?
OOP、FPはプログラムを整理する手法、つまり考え方の1つです.
そのためこれら以外の方法でプログラムを書いても実行できますが、何も計画なしでコードを書き続けるとそのうち管理が大変になったり、修正に大変な労力を必要とするクソコードになってしまうかもしれません(いわゆるスパゲッティコードと呼ばれる状態ですね).
スパゲッティコードとは、コンピュータプログラムの状態を表す俗語の一つで、命令の実行順が複雑に入り組んでいたり、遠く離れた関連性の薄そうなコード間で共通の変数が使われるなど、処理の流れや構造が把握しにくい見通しの悪い状態になっているプログラムのこと。
(IT用語辞典より)
では 効率の良くコードを整理するにはどうすれば良いのでしょうか?
この問題はプログラムを使う開発者みんなが直面しています.だからこそプログラムが1940年代に誕生してから(たぶん)、あなたよりも遥かに頭の良い先人たちが、数多くの方法を試してきました[1].
[1] History of programming languages https://en.wikipedia.org/wiki/History_of_programming_languages (Wikipedia)
ある方法は読みやすさを優先したり、ある方法は柔軟に変更しやすいように、またある方法は拡張性を重視しました.そんな数多の手法の内現在特に多くの人に支持され人気のある手法が、そうです、今回紹介するOOPとFPです.
今ならこの記事を読んでOOP、FPを学ぶことで先人つよつよプログラマーの英知をなんとタダでパクれます.といってもこの記事を書いている僕自身はクソ雑魚ナメクジなので、ナメクジに咀嚼されていないオリジナルで崇高なアイデアを知りたい人は[1]のWiki記事やオライリー様の素晴らしい書籍などを参考にするといいでしょう.
先にも書いていますがOOP、FPは考え方です.コンセプトです.なので使うプログラミング言語に限定されることではありません.(もちろんある言語によってはOOPの方が書きやすかったり、ほかの方法は書きにくかったりしますし、JavaScript
のようにOOP、FP両方使いやすい言語もありますが)
また、この界隈には某キノコタケノコ戦争並みに過激な思考を持ち、自分の支持する手法以外を全否定するおじさん(もしくはおばさん)もいるでしょうが、残念ながらきのこの山のように完璧に絶対的に他方のゴミよりも優れている手法は今のところありません(僕は知りません).手が汚れてキーボードを破壊する短小の里なんて選ぶわけないじゃん.
両方の手法を紹介しますが1つを溺愛するのではなく、峰不二子がルパンに愛想を振りつつ悪党とも手を組むように複数の候補を都合よく使えるだけ使いましょう.
関数型プログラミング:FP
前置きが長くなりましたが、それではFPについて紹介します.
FPのイメージは小さな部品(Function, 関数)をたくさん用意して必要に応じて、クソガキやクソじゃないガキや、良い年した大人が大人の○○っていいよねとか自分に言い聞かせて遊んでいる積み木のように組み合わせて使う感じです.
Photo by Nathan Dumlao on Unsplash
まあ、LEGOみたいなもんです < 積み木じゃないじゃん
ポイントは部品となる各関数をできるだけシンプルにすることです.LEGOでも色々な小さな部品をたくさん持っていると好き勝手に想像次第で色々作れますよね.逆にデス・スターを作れるキットではデススターを作るにはいい感じな特別に加工された部品はありますが、それをデススター以外に使うのは向いていないって思いませんでした?思いましたよね.そう、向いてないんです.
Photo by Xavi Cabrera on Unsplash
FPで目指すのはこんなLEGOのように小さくて使いやすい関数をたくさん用意することです(だと僕は思います)
関数型プログラミングで使いたい関数のポイント
じゃあ具体的にどんな関数がいいのさ?
ポイント1:1 task
各関数は1つのことだけやります(そうなるまで分解しましょう).
スマホと運転は同時にはできません.無理すると某不思議医者みたいに事故にあって指の震えが止まらなくなって得意な手術ができなくなって同僚に冷たく当たった挙句髭ぼうぼうに生やした状態でつよつよハゲ女に幻覚みせられて最終的には宇宙ゴリラとタイマンする羽目になりますからね.
Might as Well Screw Up Better by Very Demotivational
ポイント2:再現性(同じinput -> 同じoutput)
文字通りです.同じinputを与えれば何千回実行しても同じoutput, 戻り値になるようにしましょう.
ポイント3:NO副作用
ここでいう副作用、side effectとは関数の外の環境のことです.関数を実行するたびに、例えばグローバル変数が変わってしまうのは避けましょう.
これを避けるには
- 操作したいデータのコピーをつくる
- -> いろいろ
- -> 計算結果を戻り値として返す、
とするといいでしょう.
でもコピーを作るとメモリー消費しちゃう!助けてドラえもん~って思いませんでした?
大丈夫のび太君.こんなこともあろうかと用意しておいたものがあるんだ.
それは...
コウゾウキョウユウー Structural Sharing
オリジナルと違う部分だけメモリーに新しくつくって、同じ部分はメモリーを共有すればいいってこと.頭いいね.
あとは関数内で変数を作るといいらしいよ.closure
ってやつだね.そうすると変数たちがそれぞれの関数でしか使えないから意図しない変更を防げるってわけ.やっぱ先人は頭いいね.
ポイント4:引数と出力の数は少なく(simple is the best)
これも文字通りです.特に後で説明するFP最大の利点、複数の関数を組み合わせるcompose
を行う際に重要になってきます.引数argument
は1個、多くても2、3個に抑えましょう.
argument
を減らすにはcurrying
というテクニックが使えます.
Getting started with Functional Programming by Anwesha Chatterjee: https://medium.com/@anweshachatterjee/getting-started-with-fp-609575254919
currying
について説明しています.
currying
とは
関数を分解して引数を1つずつ取るように分解することです.
イメージとしてはこんな感じ
f(a, b, c, ...) => f(a)(b)(c)...
習うより慣れよ、というわけでコードを見てみましょう.
例えば引数を2つ受け取ってその合計を返すadd(a, b)
という関数やその掛け算を返すmulti(a, b)
は
const add = (a, b) => a + b;
const multi = (a, b) => a * b;
引数を一つずつ取るように下のように分解できます.シンプルですね
const curriedAdd = (a) => (b) => a + b;
const curriedMulti = (a) => (b) => a * b;
console.log(curriedAdd(2)(3)) // 5
console.log(curriedMulti(2)(3)) // 6
currying
の利点として途中で止めることで色々な派生形の関数を作れることがあります.
例えばcurriedAdd
を途中で止めることでadd5()
やadd10()
を作れます.このようにして作成した関数add5
などは、プログラムの中で他の関数と同じように使えるわけです.便利ですね.
const add5 = curriedAdd(5)
const multi2 = curriedMulti(2)
console.log(add5(5)) // 10
console.log(multi2(3)) // 6
ポイント5:他の関数と組み合わせ(compose)
では、先ほど作ったadd5()
, multi2()
を組み合わせて引数を2倍してその後5を足した値を返す関数multi2AndAdd5()
を作ってみましょう.
そのためには指定した関数を組み合わせて新しい関数として返す関数が必要ですね(関数って単語使いすぎ).要は関数f
と関数g
を組み合わせた内容を行う新しい関数composedFunction
を返す仕組みを作りたいってことです.
イメージとしては
composedFunction = compose(f, g)
みたいなことがしたいわけです.
JavaScript
にはcompose
を行うライブラリがあります.Ramda
とかですね.
2つだけ組み合わせるならそこまで難しくないので今回は自分で作ってしまいましょう.
というわけで2つの関数を引数として、それらを順に行いその結果を戻り値として返す関数を作成する関数compose(f, g)
は
const compose = (f, g) => (a) => f(g(a))
と書けます.そんなに難しくないですね.g(a)
で関数g
の処理を行った結果を関数f
が受け取ってf
の処理を行うってことです.
今回はf
もg
も1つだけ引数をとる関数なので簡単に書けましたが、もし複数とる場合はどうするんですかね(知らないけど)?
両方とも例えば2つの引数を取る場合はa
の部分をa, b
とすればいけそうですけど、a
とb
の順番とか、どうやって保障するんですかね(知らないけど)?
それにf
, g
の受け取る引数の数が違ったりしたらもっと面倒ですよね.
ということでcompose
したいなら引数は極力少なく、戻り値も1つだけにしたいってことです.
そうなるまで関数を分解するテクニックの1つがcurrying
ですね.
ちょっと脱線しましたがcompose()
を使って、先ほど作ったadd5()
, multi2()
を組み合わせて引数を2倍してその後5を足した値を返す関数multi2AndAdd5()
を作ってみましょう.
const multi2AndAdd5 = compose(add5, multi2)
console.log(multi2AndAdd5(5)) // 5 * 2 + 5 = 15
こうして例を見てみるとcompose
もそんな難しくないですよね.こうやってcompose
で作った関数も他の普通の関数みたいにプログラムの他のところで繰り返し使えるわけです.便利
まとめ
今回はクソみたいな例でしたのでもう少しマシなものを使っておさらいしましょう.
例えば機械学習の前処理で画像を編集する時などに、前処理全てを1つの関数にまとめるのではなく、画像を引数として処理結果の画像を返す関数をいくつも用意して組み合わせる書き方にすることでそれぞれの関数が短く済んで管理しやすくなりそうですよね.
シンプルで1つのことをNO副作用でやる関数を用意して
- applyFilter(img)
- removeNoise(img)
- compress(img)
- ...
これらを組み合わせた関数を作る
applyPreprocess = compose(applyFilter, removenoise, compress, ...)
applyPreprocess(img)
気に食わない処理過程があればcompose
から除けばいいし、簡単に他の処理も追加したり順番変えたり融通利きそうですよね.
この記事で伝えたかったというか、僕の考えは、compose
のコンセプト、もとい関数型プログラミングを知っていることで、小さい関数を沢山用意して必要に応じてそれらを組み合わせて使う方法もあるよな、って頭の引き出しに使えるアイデアが持てるようになるとイイよねってことです.
次オブジェクト指向について書こうかなって思ったけど二番煎じどころじゃないし.いらないかな...