関数型プログラミング入門(2) - FPとは

ここで述べるFPはFinancial Planner検定ではなくて関数型プログラミングのFP(Functional Programming)です。FPは関数を組み合わせたプログラミング手法だそうです。

はじめにこれを聞いた時、私は思った。今までの構造化プログラミングはそもそも関数の組み合わせだったのではないか?C言語もJavaも関数を組み合わせてプログラムを作っている。それらとこれから学ぶ関数型プログラミングの本質的な違いはなんだろうか。

Scala 関数型デザイン&プログラミング(以降FPinScalaと記載、ただし参考にしてるのは日本語訳版)では、

関数型プログラミングでは純粋関数(pure function)だけを使ってプログラムを構築することが前提となります。純粋関数とは副作用のない関数のことです。

とある。副作用のない関数とは、関数の出力が引数として与えられた値のみによって決定され、また内部処理を行う過程でプログラムのその他の部分に何ら影響を及ぼさないような関数である。逆にインスタンスのメンバ変数(フィールド)やグローバル変数を参照して何かの処理を行うような関数は副作用のある関数である。

たとえば、Setter / Getterは純粋関数ではない。下記のsetAgeは関数外のageという変数の状態を書き換えている。これが副作用である。

[scala]
class Person(a: Int, n: String){
var age: Int = a
var name: String = n

def sayHello: Unit = {
println(s"Hello, this is $name I am $age yrs old")
}

def setAge(age : Int): Unit ={
this.age = age
}
}

object NonFP{
def main(args: Array[String]) : Unit ={
val p1 = new Person(10, "akira")
p1.sayHello
p1.setAge(11)
p1.sayHello
}
}
[/scala]

実行結果

Hello, this is akira I am 10 yrs old
Hello, this is akira I am 11 yrs old

以下は純粋関数といえる。

[scala]
case class CCPerson(age: Int, name: String)
object PersonFormatter{
def helloMessage(p: CCPerson) : String =
s"Hello, this is ${p.name} I am ${p.age} yrs old"
}

object FPMain {
def main(args: Array[String]): Unit ={
val p1 = CCPerson(10, "akira")
println(PersonFormatter.helloMessage(p1))
val p2 = p1.copy(age = p1.age + 1)
println(PersonFormatter.helloMessage(p2))
}
}
[/scala]

実行結果

Hello, this is akira I am 10 yrs old
Hello, this is akira I am 11 yrs old

懸命な読者のみなさんは「あれ?printlnって副作用じゃね?」と思ったのではないでしょうか。そうです。標準出力の内部状態を変化させているのでこれは厳密に言えば副作用のある関数だ。というか、I/O処理は本質的に内部状態を変化させる副作用のある処理である。関数型プログラミングでI/Oを扱うには、たとえばHaskellだとIOモナドという仕組みがあるそうだ。ここではまだprintln程度は勘弁してほしい。

ある関数が副作用を伴うかどうか判定するとき、参照透過性が保たれているかどうかを確かめるとわかりやすい。参照透過性の定義について、FPinScalaでは下記のように書かれている:

式eがあり、すべてのプログラムpにおいて、pの意味に影響をあたえることなく、p内のすべてのeをeの評価結果と置き換えることができるとしたら、eは参照透過です

関数型プログラミングでは副作用のない関数のみを組み合わせてプログラムを構築するスタイルを取る。副作用は常に悪ではないが、副作用を伴わない関数のみを用いることで以下のようなメリットが生まれる。

  • 可読性の高さ(コードはhow to get itよりもwhat you wantを表現している)
  • コードが簡潔になる
  • 並列計算が容易に実現できる(≒マルチコアCPUを100%まで使ってくれる)
  • テスタビリティの高さ(同時にモックも容易に作れる)

特に私が大きなメリットだと思うのはコードが簡潔になるという点だ。私は大量のコードを書くのは多くの場面で悪だと思っている。同じことを実現する短いコードと長いコードがあったとき、ほとんどの場面で短いコードのほうが偉い。人はコードを書けば書くほどバグを生み出す。コードを書く量が少なければバグも少なくなるし、テストコードやテストにかかる手間も減る。もちろん、過度に簡潔な表記、たとえばほとんどすべての処理を長いワンライナーで書くことが正義だとは思っていない。何事にも限度がある。

まとめると、関数型プログラミングは

  • 副作用のない純粋関数(pure function)だけを使ってプログラムを構築する
  • ある関数が参照透過であるとき、それは純粋関数

となる。

1回めは関数型プログラミングのさわりを説明したつもりだ。次回以降はラムダ式と高階関数について述べたいと思う。