コップ本をやる 第9章 制御の抽象化

高階関数のお話です。
高階関数はSICPでやったのでさらっと流します。

object FileMatcher{
  private def filesHere = (new java.io.File(".")).listFiles

// ファイル名がqueryで指定した文字列で終わっているファイルの一覧を取得する関数
  def filesEnding(query: String) =
    for (file <- filesHere; if file.getName.endsWith(query))
      yield file
}

“ファイル名が指定した文字列で終わっている”ファイルの一覧の他に、”文字列を含んでいる”や、”正規表現がマッチする”も追加したいとします。
そのたびにfor文を含めたメソッドをコピーするのはよろしくありません。
ではどうするかというと、条件に一致するかどうか判断する関数をパラメータとすることで統一することができます。

def filesMatching(query: String, matcher: (String, String) => Boolean) = {
  for (file <- filesHere; if matcher(file.getName, query))
    yield file
}

共通化ができたので、それを使うそれぞれの関数は次のようになります。
def filesEnding(query: String) =
filesMathing(query, .endsWith())

def filesContaining(query: String) =
filesMathing(query, .contains())

def filesRegex(query: String) =
filesMathing(query, .matches())

.endsWith()は一見謎なコードですが、次のコードをプレースホルダーを使って簡潔に書いたものです。
(fileName: String, query: String) => fileName.endsWith(query)

二つのパラメータfileNameとqueryは、それぞれ1回しか使わないので、プレースホルダーにする事ができます。
すると次のコードに短縮できるわけです。
.endsWith()

クロージャーを使うことでさらに短く書けます。
filesMathing(query, .endsWith())のqueryと、二つ目のアンダースコアは同じものを指しており、二つ目のアンダースコアはqueryと書けます。
するとqueryをパラメータとして指定する必要がなくなるので、次のようになります。

private def filesMatching(matcher: String => Boolean) =
  for (file <- filesHere; if matcher(file.getName))
    yield file
def filesEnding(query: String) =
  filesMathing(_.endsWith(query))

プレースホルダー構文による関数リテラルと、クロージャーによる自由変数の束縛でここまで短くできるわけですね。
この例では、API実装側が重複コードを削減すると言う観点から高階関数を使いました。
次はクライアント側の観点で高階関数を使うメリットを紹介します。

まず、Int型を含むリストの中に、負数が含まれているかをチェックするcontainsNegと言う関数を考えます。

def containsNeg(nums: List[Int]): Boolean = {
  var exists = false
  for (num <- nums)
    if (num < 0)
      exists = true
  exists
}

このコードを、値が存在するかチェックする関数をパラメータとして取るexistsメソッドを使うことで簡潔にします。
def containsNeg(nums: List[Int]): Boolean = nums.exists(_ < 0)

Scala標準ライブラリには、このような高階関数でループ処理を簡潔に書くことができるメソッドが多数あります。

カリー化

カリー化とは、二つ以上のパラメータを取る関数を、一つのパラメータを取る関数に変換する事です。

// カリー化していない普通の関数
scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (x: Int, y: Int)Int

scala> plainOldSum(1, 2)
res0: Int = 3

//カリー化された関数
scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (x: Int)(y: Int)Int

scala> curriedSum(1)(2)
res1: Int = 3

イメージ的には、curriedSumは、パラメータを一つ取り、”パラメータを一つ取る関数”を返して、それに二つ目の引数を作用させていると言う感じです。
次のようにすれば、部分適用された途中の関数を取り出すことができます。
scala> val onePlus = curriedSum(1)_
onePlus: Int => Int =

scala> onePlus(2)
res2: Int = 3

カリー化は一見意味が無いように見えますが、関数型言語では重要な要素です。
関数型のプログラミング技術についてはScala関数型デザイン&プログラミングを読むとよりよいかと思います。

新しい制御構造を作る

一人前の値としての関数(first class method)を持つ言語では、引数として関数を取るメソッドを使うことで新しい制御構造を作ることができます。

次は、ファイルと”どう書くか”の関数をパラメータとして受け取り、ファイルをオープンして書き込んでクローズする関数です。
関数にリソースを貸し出すので、ローンパターン(loan pattern)と呼ばれているパターンです。

def withPrintWriter(file: File, op: PrintWriter => Unit) {
  val writer = new PrintWriter(file)
  try {
    op(writer)
  } finally {
    writer.close()
  }
}

// 次のように使います。
withPrintWriter(
  new File("data.txt")
  writer => writer.println(new java.util.Date)
}

Scalaは、「引数を一個だけ渡すメソッド呼び出しは、引数を囲む括弧を中括弧に変えて良い」と言う仕様があります。
そこで、先ほどのカリー化と組み合わせると、上記のコードは次のように書くことができます。

def withPrintWriter(file: File)(op: PrintWriter => Unit) {
  val writer = new PrintWriter(file)
  try {
    op(writer)
  } finally {
    writer.close()
  }
}

// 次のように使います。
val file = new File("data.txt")
withPrintWriter(file) {
  writer => writer.println(new java.util.Date)
}

まるで言語に組み込まれた制御構造のようです!

名前渡しパラメータ

名前渡しパラメータは、遅延評価される形で式を関数に渡すことができます。
次のアサーションを行うメソッドを考えます。

// 名前渡しパラメータを使わないバージョン
var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
  if (assertionsEnabled && !predicate())
    throw new AssertionError

//これは次のように使えます。
myAssert(() => 5 > 3)

// 名前渡しパラメータを使うバージョン
def myAssert(predicate: => Boolean) =
  if (assertionsEnabled && !predicate)
    throw new AssertionError

// 次のようにあたかも組み込みの制御構造のように使えます
myAssert(5 > 3)

名前渡しパラメータを使うと、関数に作用させるときに評価するのでは無く、式を実際に使うまで評価を遅延させることができます。
名前渡しにするには、() => Booleanではなく、=> Booleanと()を記述しないことで指定できます。

No Comments

Post a Comment

コメントを投稿するには、下の計算の答えを入力する必要があります。答えは半角数字で入力してください。 * Time limit is exhausted. Please reload the CAPTCHA.