読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

参考文献

dwango.github.io

11. 関数

Scalaの関数

Scalaの関数は,Function0〜22までのトレイトの無名サブクラスのインスタンスになる。
例えば,2つの整数を取って加算した値を返すadd関数は次のように定義される。

scala> val add = new Function2[Int, Int, Int] {
     |   def apply(x: Int, y: Int): Int = x + y
     | }
add: (Int, Int) => Int = <function2>

scala> add.apply(100, 200)
res17: Int = 300

scala> add(100, 200)
res18: Int = 300

Function0〜22までの全ての関数は,引数の数に応じたapplyメソッドを定義する必要がある。
applyメソッドはScalaコンパイラから特別扱いされ,x.apply(y)はx(y)のように書くことができる。

無名関数

前項の関数の定義方法では,コードが冗長になり過ぎる。
そこで,ScalaではFunction0〜22までのトレイトのインスタンスを生成するための糖衣構文が用意されている。

scala> val add = (x: Int, y: Int) => x + y
add: (Int, Int) => Int = <function2>

ここで,上記の例では変数addに関数オブジェクトが入っているだけで,関数本体に名前が付いているわけではないことに注意。
このaddの右辺のような定義をScalaでは無名関数と呼ぶ。
無名関数は単なるFunctionNオブジェクトなので,自由に変数や引数に代入したり,返り値として返すことができる。
このような性質を関数が第一級の値(First Class Object)であると言う。

無名関数の一般的な構文は以下のようになる。

(n1: N1, ... , nn: NN) => B

無名関数の返り値の型は,通常はBの型から推論される。

関数の型

(n1: N1, ... , nn: NN) => B

となるような関数の型は,FunctionN[N1, ..., NN, Bの型]と書く代わりに,

(N1, ..., NN) => Bの型

と記述することができる。

関数のカリー化

関数型言語ではカリー化というテクニックがよく使われる。
例えば,(Int, Int) => Int型の関数のような引数を複数取る関数について考える。この関数を,Int => Int => Int型の関数のように,1つの引数を取って残りの引数を取る関数を返す,関数のチェインで表現する。

scala> val add = (x: Int, y: Int) => x + y
add: (Int, Int) => Int = <function2>

scala> add(100, 200)
res21: Int = 300

scala> val addCurried = (x: Int) => ((y: Int) => x + y)
addCurried: Int => (Int => Int) = <function1>

scala> addCurried(100)(200)
res22: Int = 300

scala> addCurried(100)
res23: Int => Int = <function1>

scala> res23(200)
res24: Int = 300

よく見ると,無名関数を定義する構文をネストさせているだけで,特別なことは何もしていないことがわかる。
なお,Scalaではメソッドの引数リストを複数に分けることで,簡単にカリー化できる。

scala> def add(x: Int, y: Int): Int = x + y
add: (x: Int, y: Int)Int

scala> add(1, 2)
res1: Int = 3

scala> def addCurried(x: Int)(y: Int): Int = x + y
addCurried: (x: Int)(y: Int)Int

scala> addCurried(1)(2)
res2: Int = 3

メソッドと関数の違い

本来はdefで始まる構文で定義されたものだけがメソッドだが,説明の便宜上,所属するオブジェクトのないメソッドやREPLで定義したメソッドを関数と呼ぶようなことがある。
書籍やWebでもこの2つを意図的または無意識に混同している例があるので,注意が必要。

再度強調すると,メソッドはdefで始まる構文で定義されたものであり,関数と違って第一級の値ではない。よって,メソッドを取る引数やメソッドを返す関数,メソッドが入った変数といったものはScalaには存在しない。

高階関数

関数を引数に取ったり,関数を返すメソッドや関数のことを高階関数と呼ぶ。
(この場合はメソッドも関数と呼ぶことになる)

scala> def double(n: Int, f: Int => Int): Int = {
     |   f(f(n))
     | }
double: (n: Int, f: Int => Int)Int

上記の例は,与えられた関数fをnに対して2回適用する関数doubleになる。
ちなみに,高階関数に渡される関数は適切な名前を付けられないことも多く,その場合はfやgなどの1文字の名前が良く使用される。
呼び出しは以下のようになる。

scala> double(3, m => m * 2)
res4: Int = 12

scala> double(3, m => m * m)
res5: Int = 81

scala> val sanbai = (x: Int) => x * 3
sanbai: Int => Int = <function1>

scala> double(4, sanbai)
res6: Int = 36

もう少し意味のある例を出してみると,プログラムを書く時の頻出パターンである,初期化・処理・後処理について,これをメソッドにした高階関数aroundが定義できる。

scala> def around(init: () => Unit, body: () => Any, fin: () => Unit): Any = {
     |   init()
     |   try {
     |     body()
     |   } finally {
     |     fin()
     |   }
     | }
around: (init: () => Unit, body: () => Any, fin: () => Unit)Any

try〜finally構文は後の節でも出てくるが,大体Javaのものと同じだと思って良い。
このaroundメソッドは以下のように使うことができる。

scala> around(
     |   () => println("open file"),
     |   () => println("edit file"),
     |   () => println("close file")
     | )
open file
edit file
close file
res1: Any = ()

aroundに渡した関数が順番に呼ばれていることがわかる。
ここで,bodyの部分で例外を発生させてみると,

scala> around(
     |   () => println("open file"),
     |   () => throw new Exception("例外発生!"),
     |   () => println("close file")
     | )
open file
close file
java.lang.Exception: 例外発生!
  at $anonfun$3.apply(<console>:15)
  at $anonfun$3.apply(<console>:15)
  at .around(<console>:14)
  ... 46 elided

finally句に記載したfinの部分がしっかりと実行されていることがわかる。

このように,aroundという高階関数を定義することで,初期化・処理・後処理といったそれぞれの処理を値として部品化して,aroundメソッドを様々な処理に流用することができる。

例えば,Java7で実装されたtry-with-resource文は,後処理を自動化する構文だが,高階関数がある言語であれば,言語の機能に頼らずに自分でそのような働きをするメソッドを定義することができる。