新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。もう新人じゃないかも…

学習記録:Scala逆引きレシピ

内容

第3章:関数とクロージャ

関数

関数の定義は、引数と関数本体を関数矢印=>でつなげて定義を行う。
この記述を関数リテラルと呼び、定義された関数を無名関数と呼ぶ。

// Int => Int型の変数fに、整数iを引数にとってiの2倍の整数値を返す関数を代入
val f: Int => Int = (i: Int) => i * 2
// 引数の型が推論できる場合省略可
val f: Int => Int = (i) => i * 2
// 引数の型を省略しかつ引数が1つの場合、丸括弧も省略可
val f: Int => Int = i => i * 2
// 引数の使用が一回のみの場合、プレースホルダ構文を利用可能
// n番目のプレースホルダがn番目の引数となり、引数は定義順に一回のみ使用する
val f: Int => Int = _ * 2
val f: (Int,Int) => Int = _ * 2 + _
// 引数なしの場合
val f = () => println("hoge")

Scalaでは関数とメソッドは厳密には同じではなく、defで定義したものはメソッドで、ファーストクラスオブジェクトではない。
一方、defで定義していないものは関数で、上の例のように変数に入れたり引数に渡したりできる=ファーストクラスオブジェクトである。
ただし、実際はdefで定義したメソッドも関数として扱うことが可能なので、それほど区別して扱うことはない。

// メソッドの定義
def printName(name: String):String = "名前:" + name

// これはできない
val f = printName
// 代入先が関数型として定義されていれば自動で関数に変換される
val f: String => String = printName
// 代入先が関数型として明示的に定義されていない場合、メソッド名の後ろに_を付けると関数に変換される
// このような形を部分適用と呼ぶ。詳細は後述
val f = printName _

関数を戻り値として返す関数を定義することもできる。

// 「整数型の引数を取りiその2倍の値を返す関数」を返す関数
def double = (i: Int) => i * 2
// doubleの引数(引数なし)と戻り値の型を省略しないと以下のようになる
def double(): Int => Int = (i: Int) => i * 2

Javaではメソッドの可読性を向上させるため、privateメソッドを定義して関数の利用される場所を限定することがある。
そのような場合、Scalaではローカル関数を使用することでどの関数がどこで利用されるかをより明確にできる。

クロージャ

クロージャとは、関数の生成時に、外部の変数を取り込んで動作する関数のこと。
取り込む変数を自由変数と呼ぶ。

// 先の例で使用した「整数型の引数を取りiその2倍の値を返す関数」を返す関数。
// 以下の関数が使用しているのは引数iのみで、クロージャではない
def double = (i: Int) => i * 2

// 以下の関数の戻り値となる無名関数がクロージャ
// 自由変数はtimes
def multi(times: Int) = (i: Int) => i * times

// 自由変数に10を代入したクロージャを作成
val tentimes = multi(10)
tentimes(5) // => 50
// 生成したクロージャを直接呼び出し
multi(100)(5) // => 500
部分適用

Scalaでは、ある関数が必要とする引数のうち、一部の引数のみを受け取って処理を行う新たな関数を作成できる。
これを関数の部分適用と呼び、省略する引数には_を記述する。

def multi(num: Int, times: Int) = num * times

// 第1引数のみ省略
val f1 = multi(_: Int, 100)
// numのみ渡す
f1(50) // => 5000

// 全ての引数を省略
// 少し上で書いたメソッドから関数への変換の書き方はこれ
val f2 = multi _

また、Scalaでは関数を部分的に定義できる部分関数がある。
例えばパターンを再利用したい場合には、下記のようにPartialFunctionを利用する。

// 変数baseに「例外の型によって表示する文字列を返す」部分関数を代入する
val base: PartialFunction[Throwable, String] = {
  case _: IllegalArgumenException => "Parameter is invalid"
  case _: IllegalStateException => "State is invalid"
}

// 引数の整数が0以上かチェックする関数
def check(i: Int) = {
  try {
    if (i < 0) throw new IllegalArgumentException
    "success"
  } catch base // catch部分に部分関数baseを使用
}
カリー化

カリー化とは、引数が複数ある関数を1つの引数を持つ関数のチェーンとして呼び出せるように変換することである。
引数に関数リテラルのような制御構造を渡すような場合に、カリー化を行うことでコードの意図を明確にし可読性を向上させる。

// 「String型のリスト」と「String型のリストを引数にString型を返す関数」を引数に、後者の引数に前者を入れて呼びだす関数
def execute(data: List[String], f: List[String] => String) = f(data)

// このメソッドの呼び出しは下記のようになるが、値と制御構造が混在してわかりにくい
execute(List("Alice","Bob","Curl"), data => "NameList: " + data.toString()) // => NameList: List(Alice, Bob, Curl)

// メソッドをカリー化する
def execute(data: List[String])(f: List[String] => String) = f(data)

// メソッドの呼び出し時に引数が1つずつになる・・・がこれだとまだわかりにくい
execute(List("Alice","Bob","Curl"))(data => "NameList: " + data.toString())
// 呼び出しに波括弧を使うことで、1つのブロックとして制御構造を渡すよう明確に見せることが可能
execute(List("Alice","Bob","Curl")) { data => 
  "NameList: " + data.toString()
}

// なお、メソッドのカリー化は引数に()()と引数リストを続けて記述したが、
// 関数のカリー化は以下のように=>を続けて記述する
val func = (title: String) => (name: String) => title + name

なお、サードパーティAPIをカリー化したいような場合には、curriedメソッドを使う。

// 定義された関数executeをカリー化する
val curryfunc = execute.curried
// メソッドをカリー化する
val currydef = (execute _).curried
再帰

複雑なループ処理を簡潔に記述する方法として、Scalaでは再帰を使用する。
この際注意することは、末尾再帰=処理の一番最後で自分自身を呼ぶようにすることである。
通常、関数を呼びだすごとに新しいスタックフレーム=変数などの状態を一時保存しておく一時領域が確保されるため、
再帰では同一関数に対するフレームが多数生成され、深い再帰ではスタックオーバーフローが発生する可能性がある。

しかし、末尾再帰であればScalaコンパイラによって単一のスタックフレームで実行されるように最適化され、
スタックオーバーフローになる心配がなく、再帰による実行時のオーバヘッドも解消される。
(何故なら、末尾再帰では再帰呼び出しがその関数の処理において最後に呼び出される式であるため、
後続の処理が存在しないため呼び出し前のスタック状態を保持しておく必要がなく、単純なループに変換できるためである)

// 末尾再帰を利用したループ処理の例
// 整数リストの合計値を算出する
def sum(totla: Int, list: List[Int): Int = {
  if (list.isEmpty) total
  else sum(total + list.head, list.tail)
}

複数の関数が順にお互いを呼び出しあって再帰することをトランポリンと呼ぶ。
トランポリンを利用したい場合には、scala.util.control.TailCallsを使用する。

import scala.util.control.TailCalls._

// 整数のリストの値の一つを現在値totalに加算する。
// 末尾再帰する関数はtailcalメソッドに渡し、戻り値はdoneメソッドに渡してTailRecクラスにラップする
def plus(total: Int, list: List[Int]): TailRec[Int] = {
  if (list.isEmpty) done(total)
  else tailcall(minus(total + list.head, list.tail))
}

// 整数のリストの値の一つを現在値totalに減算する。
// 末尾再帰する関数はtailcalメソッドに渡し、戻り値はdoneメソッドに渡してTailRecクラスにラップする
def minus(total: Int, list: List[Int]): TailRec[Int] = {
  if (list.isEmpty) done(total)
  else tailcall(plus(total - list.head, list.tail))
}

plus(0, List(100, 60, 30)).result // => 70 (100 - 60 + 30の結果)

なお、Scalaのトランポリンはコンパイラによる最適化ではなく、上記のようにライブラリとして実装されている。
tailcallメソッドやdoneメソッドの実行によるオーバヘッドが発生することに注意。