関数型プログラミングの話

まえがき

最近関数型プログラミングについての話題をよく耳にするので、その波に乗って自分も関数型プログラミングに対する理解を書いてみる。

コード例は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.idvを受け取って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以外にも関数が第一級オブジェクトでなければ不可能だったコードの分解・モジュール化を数多く行い活用している。

そんなわけで

最初の話に戻って、自分は関数型プログラミングとは「関数を第一級オブジェクトにすることにより可能になったモジュール性の高いプログラミングスタイル」だと思っている。もちろん関数型プログラミングはそれだけじゃないけど、これを軸に説明がつく特徴が多いと思ってる。

*1:関数を値として扱えるようにする、ということ。関数を変数に代入出来るし関数を関数の引数に渡すことも出来る

*2:手続き型と関数型の比較のためscalaで書いてるけど、scalaでこういうコードは(パフォーマンス等の理由がない限り)あんまり書かない

*3:手元の実行環境が2.10.3だった