関数型プログラミング入門(4) - 高階関数

高階関数とは、関数を引数にとる関数です。特にこれは難しい概念ではありません。特に関数型プログラミング特有というものでもなくて、たとえばJavaScript(jQuery)イベントハンドラを設定するときに使用した経験が必ずあるはずです。

[js]
$("#button").click(function() { alert("hello!"); })
[/js]

jQueryのclickメソッドは高階関数と言えます。

高階関数を使うと何が嬉しいかというと、処理を可変にできる(抽象化できる)という点が嬉しいです。どういうことか順を追って考えていきましょう。いま、従業員を表すEmployeeというクラスがあったとします。

[scala]
case class Employee(age: Int, gender: String, name: String, skillSet: Set[String])
object Employee{
def createSample: Seq[Employee] =
Employee(24, "M", "Ahoyama Takeru", Set("Java", "C#")) ::
Employee(30, "F", "Ushijima Kanemi", Set("Java", "Financial")) ::
Employee(34, "M", "Nekoyama Inurou", Set("Java", "Scala")) ::
Employee(22, "F", "Machida Machiko", Set("Scala", "JS")) ::
Employee(36, "M", "Kaneda Tetsuo", Set("Scala", "ESP")) ::
Employee(35, "F", "Namba Hisako", Set("C#", "JS")) ::
Employee(26, "M", "Ahoda Tarou", Set("C#")) ::
Employee(33, "F", "Mazda Hitomi", Set("Scala")) ::
Employee(40, "M", "Unko Moreo", Set("Java", "Scala")) :: Nil
}
[/scala]

いま、A君はスケベな課長の下で働くプログラマだったとします。ある日、課長から「女性従業員を検索したい」という要望があったとします。課長はスケベなオヤジなので自分の近くに女の子の席を配置したいと考えていることがその背景です。長らくJavaプログラマで最近Scalaを始めたA君は下記のようなコードを書きました。

[scala]
def findByGender(es: Seq[Employee], q: String): Seq[Employee] = {
val result = new ListBuffer[Employee]()
for(e <- es){
if(e.gender == q)
result += e
}
result
}
[/scala]

A君はコードを作成し、いい年こいていかがわしいお店にどっぷりはまってる課長に報告しました。課長は女性社員を検索しました。

[scala]
val employees = Employee.createSample
val females = EmployeeSearcher.findByGender(employees, "F")
println("find female: " + females.map(_.name).mkString(", "))
[/scala]

が、程なくしてA君は再度呼び出され、叱責されました。課長の話はいまいち要領を得なかったのですが、どうも先のコードでは全年齢の女性社員がヒットすることが問題であったようです。課長は「俺が若い子に大人の趣味や生活というものを教えてやる」などとおこがましい考えでいるようです。しかし本当のところは、課長は精神年齢が幼く、大人の女性に相手にされないからそう言ってるだけとA君は推察しています。

ともかく、年齢でフィルタリングする関数も作成しました。

[scala]
def findYoungerThan(es: Seq[Employee], q: Int): Seq[Employee] = {
val result = new ListBuffer[Employee]()
for(e <- es){
if(e.age < q)
result += e
}
result
}
[/scala]

課長は満足げにまた検索を行いました。

[scala]
val femaleAndUnder30 = EmployeeSearcher.findYoungerThan(females, 35)
println("find female under 30yrs old: " + femaleAndUnder30.map(_.name).mkString(", "))
[/scala]

しかしA君はまたもや課長に呼び出され、叱責されました。課長の話は何度聞いても要領を得ないのですが、要するに「あまり若すぎると駄目」ということが言いたいようです。いい加減仕事させてくれないかな、と思いつつA君は下記の関数を書きました。

[scala]
def findByAgeRange(es: Seq[Employee], lower: Int, upper: Int): Seq[Employee] = {
val result = new ListBuffer[Employee]()
for(e <- es){
if(e.age >= lower && e.age <= upper)
result += e
}
result
}
[/scala]

課長は新しい関数を利用して女性社員を検索し、ようやく満足したようです。

[scala]
val females = EmployeeSearcher.findByGender(employees, "F")
val female25to35 = EmployeeSearcher.findByAgeRange(females, 25, 35)
println("find female 25-30yrs old: " + female25to35.map(_.name).mkString(", "))
[/scala]

しかし1ヶ月後、課長はまたA君を呼び出しました。どうも課長は自分が入れ込んでいるプロジェクトに自身のお気に入りの女性社員を次々と投入していったようなのですが、メンバの保有するスキルセットを何も考慮していなかったため、Javaを利用した開発プロジェクトなのにJavaができる人がほとんど居ないという状況になっているようです。

A君は近くにあったキーボードでしたたか課長のはげ頭を打ちのめしたくなって来ましたが寸前のところで踏みとどまり、以下のような関数を書きました。

[scala]
def findBySkill(es: Seq[Employee], skill: String): Seq[Employee] = {
val result = new ListBuffer[Employee]()
for(e <- es){
if(e.skillSet.contains(skill))
result += e
}
result
}
[/scala]

これにて課長は25歳から35歳の人でJavaスキルをもつ女性従業員をようやく検索することができました。

[scala]
val females = EmployeeSearcher.findByGender(employees, "F")
val female25to35withJavaSkill = EmployeeSearcher.findBySkill(EmployeeSearcher.findByAgeRange(females, 25, 35), "Java")
println("find female 25-30 with Java skill: " + female25to35withJavaSkill.map(_.name).mkString(", "))
[/scala]

以上の問題点は何でしょうか?管理職として上の立場にある課長が変態なのはもちろん問題ではありますが、A君のコードにも問題があります。フィルタリング関数を数々定義していますが、その関数の大部分は同じことを繰り返しであり、異なるのはifの条件だけです。DRYの原則を思い出してください。同じことを何度も書くのは悪です。この条件の部分をどうにか抽象化したい、というのがモチベーションです。

ここで高階関数が活きてきます。「条件」を抽象化して渡すことができれば汎用的なフィルタリング関数を作れます。引数fは絞り込みたい要素ならばtrue, そうでなければfalseを返す関数を指定します。

[scala]
def findEmployee(es: Seq[Employee], f: Employee => Boolean): Seq[Employee] = {
val result = new ListBuffer[Employee]()
for(e <- es){
if(f(e))
result += e
}
result
}
[/scala]

以下のように自由に条件を組み合わせてフィルタリングを行うことができます。

[scala]
val female25to35withScalaSkill =
EmployeeSearcher.findEmployee(employees, e =>
25 <= e.age && e.age <= 35 && e.gender == "F" && e.skillSet.contains("Scala"))
[/scala]

ここで、もう一度findEmployee関数をじっくり見てください。この関数は別にEmployeeに限定されている必要はありませんよね?なので任意の型Tに対して適用できるようにします。

[scala]
def filter[T](es: Seq[T], f: Employee => Boolean): Seq[T] = {
val result = new ListBuffer[T]()
for(e <- es){
if(f(e))
result += e
}
result
}
[/scala]

はい、これで任意の型TのSeqに対して汎用的に適用できるフィルタリング関数ができました。でもまあこのように私がすぐに思いつくことは偉い先人がとっくにやっているわけで、当然のごとくfilterなどというメソッドはScala標準ライブラリにも存在します。

[scala]
// 略
* @version 2.8
* @since 2.8
* @tparam A the element type of the collection
* @tparam Repr the type of the actual collection containing the elements.
*
* @define Coll Traversable
* @define coll traversable collection
*/
trait TraversableLike[+A, +Repr] extends Any
with HasNewBuilder[A, Repr]
with FilterMonadic[A, Repr]
with TraversableOnce[A]
with GenTraversableLike[A, Repr]
with Parallelizable[A, ParIterable[A]]
{
// 中略
private def filterImpl(p: A => Boolean, isFlipped: Boolean): Repr = {
val b = newBuilder
for (x <- this)
if (p(x) != isFlipped) b += x

b.result
}
// 以下略
}
[/scala]

どうでしょう。ほぼ同じようなことをやっていることがわかりますね。

次回はほかの一般的な高階関数を紹介します・・・と言いたいところですが、こんな調子で書いてたらいつまでたっても進まないので、次回は半群とモノイドについて書きます。