無限大な夢のあと

テニスとアニメが大好きな厨二病SEのブログ

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その7(関数)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【関数】
Scalaの関数

Scalaの関数は、他の言語の関数と扱いが異なります。Scalaの関数は単に Function0 〜 Function22 までのトレイトの無名サブクラスのインスタンスなのです。

たとえば、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)
res0: Int = 300

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

Function0からFunction22までの全ての関数は引数の数に応じたapplyメソッドを定義する必要があります。 applyメソッドはScalaコンパイラから特別扱いされ、x.apply(y)は常にx(y)のように書くことができます。後者の方が関数の呼び方としては自然ですね。

また、関数を定義するといっても、単にFunction0からFunction22までのトレイトの無名サブクラスのインスタンスを作っているだけです。

関数の作り方は理解。
確かもっと簡単に作れていた印象。
省略しないで書くと、このような処理をしているのか。
トレイトの無名サブクラスのインスタンス
無名サブクラスと言われてもピンとこない。。

・無名関数

前項でScalaで関数を定義しましたが、これを使ってプログラミングをするとコードが冗長になり過ぎます。そのため、 ScalaではFunction0〜Function22までのトレイトのインスタンスを生成するためのシンタックスシュガーが用意されています。たとえば、先ほどのadd関数は

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

と書くことができます。ここで、addには単に関数オブジェクトが入っているだけであって、関数本体には何の名前も付いていないことに注意してください。この、addの右辺のような定義をScalaでは無名関数と呼びます。無名関数は単なるFunctionNオブジェクトですから、自由に変数や引数に代入したり返り値として返すことができます。このような、関数を自由に変数や引数に代入したり返り値として返すことができる性質を指して、Scalaでは関数が第一級の値(First Class Object)であるといいます。

無名関数の一般的な構文は次のようになります。

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

n1からnnまでが仮引数の定義でN1からNNまでが仮引数の型です。Bは無名関数の本体です。無名関数の返り値の型は通常は Bの型から推論されます。先ほど述べたように、Scalaの関数はFunction0〜Function22までのトレイトの無名サブクラスのインスタンスですから、引数の最大個数は22個になります。

JavaScriptの無名関数と同じイメージ。
関数が「First Class Object」であるというのはこういうことか。
ちなみに、Golangも関数は「First Class Object」です。
また、Golangジェネリクスがないので、リフレクションで頑張らないと関数型の汎用的なmap関数やreduce関数のようなことはできないのです。

・関数の型

このようにして定義した関数の型は、本来はFunctionN[...]のようにして記述しなければいけませんが、関数の型については特別にシンタックスシュガーが設けられています。一般に、

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

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

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

として記述することができます。直接FunctionNを型として使うことは稀なので、こちらのシンタックスシュガーを覚えておくと良いでしょう。

具体例のサンプルが欲しかった。
シンタックスシュガーの記述に関しては後ほど、コップ本で調べる。

・関数のカリー化

関数型言語ではカリー化というテクニックがよく使われます。カリー化とは、たとえば (Int, Int) => Int 型の関数のように複数の引数を取る関数があったとき、これを Int => Int => Int 型の関数のように、1つの引数を取り、残りの引数を取る関数を返す関数のチェインで表現するというものです。試しに上記のaddをカリー化してみましょう。

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

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

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

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

無名関数を定義する構文をネストさせて使っているだけで、何も特別なことはしていないことがわかります。

また、Scalaではメソッドの引数リストを複数に分けることで簡単にカリー化された関数を得ることができます。このことをREPLを用いて確認してみましょう。

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

scala> add _
res0: (Int, Int) => Int = <function2>

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

scala> addCurried _
res1: Int => (Int => Int) = <function1>

引数リストを複数に分けたaddCurriedから得られた関数は1引数関数のチェインになっていて、確かにカリー化されています。

Scalaのライブラリの中にはカリー化された形式の関数を要求するものがあったりするので、とりあえず技法として覚えておくのが良いでしょう。

メソッドの引数リストを複数に分けることで簡単にカリー化された関数を得ることができるのか。
Underscore.jsではそこまで便利ではなかったかなー
以下の書籍で少しだけ関数型の勉強はしたことがあったが、途中で挫折して積読になってしまっていた。。

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング

また、Golangではシンタックスシュガーは用意されていなかった。
以下の記事でカリー化のサンプルがあった。
Go言語で関数のカリー化(currying)入門 - Qiita

・メソッドと関数の違い

メソッドについては既に説明しましたが、メソッドと関数の違いについてはScalaを勉強する際に注意する必要があります。本来はdefで始まる構文で定義されたものだけがメソッドなのですが、説明の便宜上、所属するオブジェクトの無いメソッド(今回は説明していません)やREPLで定義したメソッドを関数と呼んだりすることがあります。書籍やWebでもこの2つを意図的に、あるいは無意識に混同している例が多々あるので(Scalaバイブル『Scalaスケーラブルプログラミング』でも意図的なメソッドと関数の混同の例がいくつかあります)注意してください。

再度強調すると、メソッドはdefで始まる構文で定義されたものであり、それを関数と呼ぶのはあくまで説明の便宜上であるということです。ここまでメソッドと関数の違いについて強調してきましたが、それは、メソッドは第一級の値ではないのに対して関数は第一級の値であるという大きな違いがあるからです。メソッドを取る引数やメソッドを返す関数、メソッドが入った変数といったものはScalaには存在しません。

ここはしっかりと押さえたいところ。

高階関数

関数を引数に取ったり関数を返すメソッドや関数のことを高階関数と呼びます。先ほどメソッドと関数の違いについて説明したばかりなのに、メソッドのことも関数というのはいささか奇妙ですが、慣習的にそう呼ぶものだと思ってください。

早速高階関数の例についてみてみましょう。

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

これは与えられた関数fを2回nに適用する関数doubleです。ちなみに、高階関数に渡される関数は適切な名前が付けられないことも多く、その場合はfやgなどの1文字の名前をよく使います。他の関数型プログラミング言語でも同様の慣習があります。呼び出しは次のようになります。

scala> double(1, m => m * 2)
res4: Int = 4

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

scala> double(3, m => m * 4)
res6: Int = 48

最初の呼び出しは1に対して、与えられた引数を2倍する関数を渡していますから、1 * 2 * 2 = 4になります。2番めの呼び出しは2に対して、与えられた引数を3倍する関数を渡していますから、2 * 3 * 3 = 18になります。最後の呼び出しは、3に対して与えられた引数を4倍する関数を渡していますから、3 * 4 * 4 = 48になります。

上記のようなサンプルだと、累乗計算を始め計算系の処理が楽にできそうなイメージ。
高階関数に渡される関数の名前が適切ではない1文字の名前が渡されるのは覚えておこう。

もう少し意味のある例を出してみましょう。プログラムを書くとき、

初期化
何らかの処理
後始末処理
というパターンは頻出します。これをメソッドにした高階関数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("ファイルを開く"),
     |   () => println("ファイルに対する処理"),
     |   () => println("ファイルを閉じる")
     | )
ファイルを開くファイルに対する処理ファイルを閉じる
res7: Any = ()

aroundに渡した関数が順番に呼ばれていることがわかります。ここで、bodyの部分で例外を発生させてみます。throwはJavaのそれと同じで例外を投げるための構文です。

scala> around(
     |   () => println("ファイルを開く"),
     |   () => throw new Exception("例外発生!"),
     |   () => println("ファイルを閉じる")
     | )
ファイルを開くファイルを閉じる
java.lang.Exception: 例外発生!
  at $anonfun$3.apply(<console>:16)
  at $anonfun$3.apply(<console>:16)
  at .around(<console>:15)
  ... 906 elided

のそれぞれを部品化して、「何らかの処理」の部分で異常が発生しても必ず後始末処理を実行できています。このaroundメソッドは1〜3の手順を踏む様々な処理に流用することができます。一方、1〜3のそれぞれは呼び出し側で自由に与えることができます。このように処理を値として部品化することは高階関数を定義する大きなメリットの1つです。

ちなみに、Java 7では後始末処理を自動化するtry-with-resources文が言語として取り入れられましたが、高階関数のある言語では、言語に頼らず自分でそのような働きをするメソッドを定義することができます。

後のコレクションの節を読むことで、高階関数のメリットをより具体的に理解できるようになるでしょう。

高階関数の使いどころをよくわかっていなかったけど、このような形で使うこともできるんだ。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門