関数型プログラミングの話
まえがき
最近関数型プログラミングについての話題をよく耳にするので、その波に乗って自分も関数型プログラミングに対する理解を書いてみる。
コード例はscalaで書く。知らない人にも分かるよう努力する。
関数型プログラミングってなに?
関数型プログラミングを一言で言い表すとすれば「関数を第一級オブジェクトにする*1ことにより可能になったモジュール性の高いプログラミングスタイル」だと思う。
つまりどういうことだってばよ?
関数型プログラミングの話をする時に僕はよくこう質問する
ユーザオブジェクトの配列があったとして、ユーザIDの配列が欲しいとどう書く?
例えば、以下の様な状況の時、getUserIdsの実装をどう書くか?
case class User(id: Int) // ???は未実装なことを表す def getUserIds(userArray: Array[User]): Array[Int] = ??? var userArray = Array( new User(1), new User(2), new User(3) ) getUserIds(userArray) // Array(1,2,3)が返ってきて欲しい
手続き型プログラミングしてる人の答えとして、こういうのを想定してる*2。
def getUserIds(userArray: Array[User]): Array[Int] = { var userIdArray = Array[Int]() for (user <- userArray) { userIdArray = userIdArray :+ user.id } userIdArray // scalaではreturn書かなくても最後の値が返る }
これを、関数型っぽく書くと、こうなる
def getUserIds(userArray: Array[User]): Array[Int] = userArray.map(v => v.id)
v => v.id
はv
を受け取ってv.id
を返す無名関数。map
は配列内の全ての要素に対して引数の関数を実行した配列を返すので、これでidの配列が手に入る。
少なくともこの例では関数型の方が簡潔であることは多くの人が同意してくれるんじゃないかと思う。
ここで、どうしてこう書けるのか?というのを考えてみる。ここのmap
の定義を見ると
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { def builder = { // extracted to keep method size under 35 bytes, so that it can be JIT-inlined val b = bf(repr) b.sizeHint(this) b } val b = builder for (x <- this) b += f(x) b.result }
https://github.com/scala/scala/blob/e2fec6b28dfd73482945ffab85d9b582d0cb9f17/src/library/scala/collection/TraversableLike.scala#L237-L246 *3
配列以外にも使えるように汎用的に書かれているため分かりづらいが、肝はここ
val b = builder for (x <- this) b += f(x) b.result
これはまさにgetUserIdsの手続き型版にそっくりだ。関数型版では手続き型版のコードを分解し、この面倒なところだけmap
に押し込めている。そして、これは関数を引数に取れる、つまり関数が第一級オブジェクトでないと不可能だ。
関数型プログラミングでは、map以外にも関数が第一級オブジェクトでなければ不可能だったコードの分解・モジュール化を数多く行い活用している。
そんなわけで
最初の話に戻って、自分は関数型プログラミングとは「関数を第一級オブジェクトにすることにより可能になったモジュール性の高いプログラミングスタイル」だと思っている。もちろん関数型プログラミングはそれだけじゃないけど、これを軸に説明がつく特徴が多いと思ってる。