コップ本をやる 第8章 関数とクロージャー

メソッド

オブジェクトのメンバーとして関数を定義するとメソッドになります。

object LongLines {
  def processFile(filename: STring, width: Int) {
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
  }

private def processLine(filename: String, width: Int, line: String) {
    if (line.length > width)
      println(filename + ": " + line.trim)
  }
}

ローカル関数

外部に公開しない関数として、privateな非公開メソッドがありますが、Scalaでは関数内だけで使用する関数内で定義する関数として、ローカル関数があります。
先ほどのコードは次のようになります。

  def processFile(filename: STring, width: Int) {
    def processLine(line: String) {
      if (line.length > width)
        println(filename + ": " + line.trim)
    }
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
  }

ローカル関数からは外側の関数のパラメータにアクセス可能なので、processLineのパラメータはlineだけになっています。

一人前の存在としての関数

Scalaの関数はfirst-classな関数です。
関数リテラル(function literals)はあるクラスにコンパイルされ、実行時にインスタンス化すると値としての関数(functionvalues)になります。
関数リテラルはソースコード上だけの存在で、関数値(値としての関数)はオブジェクトとして存在します。

次はInt型を取り1加算する関数リテラルの例です。
(x: Int) => x + 1

値としての関数はオブジェクトなので、変数に格納することができます。変数に格納しても関数なので通常の関数のように呼び出せます。

scala> var increase = (x: Int) => x + 1
increase: Int => Int =

scala> increase(10)
res0: Int = 11

関数に複数行コードを入れたい場合、中括弧で囲みます。その関数が返す値はブロック内の最終行の式が生成する値です。
scala> var increase = (x: Int) => {
x + 100
x + 10
x + 1
}
increase: Int => Int =

scala> increase(100)
res2: Int = 101

Scalaの多くのライブラリは、この関数リテラルと値としての関数をうまく使えるよう設計されています。
たとえばすべてのコレクションにはforeashメソッドが定義されており、foreachメソッドは関数をとってそれぞれの要素についてその関数を呼び出します。
scala> val someNumbers = List(-11, 10, 0, 100)
someNumbers: List[Int] = List(-11, 10, 0, 100)

scala> someNumbers.foreach((x: Int) => println(“value=” + x))
value=-11
value=10
value=0
value=100

コレクションについては17章で詳細に説明があります。

関数リテラルの短縮形

関数リテラルのパラメータの型は省略可能です。
scala> someNumbers.filter((x) => x > 0)
res4: List[Int] = List(10, 100)

パラメータは(x: Int)ですが、(x)と書けます。

さらに、型推論が可能な場合はパラメータの括弧も省略できます。
scala> someNumbers.filter(x => x > 0)
res5: List[Int] = List(10, 100)

プレースホルダー構文

もっと関数リテラルを簡潔に書くことができます。
パラメータが関数リテラル内で一度しか使われない場合は、アンダースコアでパラメータを使うことができます。
scala> someNumbers.filter(_ > 0)
res6: List[Int] = List(10, 100)

ただし、プレースホルダー構文を使うには型推論が可能である必要があります。
型推論できない場合はエラーになります。

scala> val f = _ + _
:10: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2))
val f = _ + _
^
:10: error: missing parameter type for expanded function ((x$1: , x$2) => x$1.$plus(x$2))
val f = _ + _
^

そんな場合でも型を指定してあげるとOKです。
scala> val f = (:Int) + (:Int)
f: (Int, Int) => Int =

scala> f(1, 2)
res7: Int = 3

部分適用された関数

上記のアンダースコアは個別のパラメータの代わりとなるものです。
アンダースコアは他にも、パラメータ全体の代わりとして使うこともできます。

scala> someNumbers.foreach(println _) // パラメータ全体をアンダースコアに置き換えている。
-11
10
0
100

scala> someNumbers.foreach(println(_)) // 最初のパラメータをアンダースコアに置き換えている
-11
10
0
100

次の例では3つのパラメータの合計を返すsum関数を考えます。
scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> val a = sum _
a: (Int, Int, Int) => Int =

scala> a(1, 2, 3)
res11: Int = 6

sum _とすると、0個のパラメータを部分適用した関数を返します。
そしてその自動生成された関数にパラメータを入れると元の関数を実行することができます。

内部的には3つのパラメータを取るapply関数が定義された関数オブジェクトを生成しており、a(1, 2, 3)はa.apply(1, 2, 3)を実行していることになります。
scala> val a = sum _
a: (Int, Int, Int) => Int =

scala> a.apply(1, 2, 3)
res15: Int = 6

次のようにしてパラメータの一部を埋めた状態で残りのパラメータを入力させる関数を作ることもできます。
この場合1つのパラメータを取るapply関数を定義した関数オブジェクトを生成しています。
scala> val a = sum(1, _: Int, 3)
a: Int => Int =

scala> a(2)
res12: Int = 6

もしforeachのように、関数呼び出しが必要とされている部分ではアンダースコアさえも省略できます。
scala> someNumbers.foreach(println)
-11
10
0
100

関数呼び出しが必要とされている場所以外でこの記法を使うとエラーになります。
scala> val a = sum
:11: error: missing arguments for method sum;
follow this method with `_’ if you want to treat it as a partially applied function

クロージャー

今までは関数は関数のパラメータだけを参照していましたが、関数の外の変数を参照することも可能です。

もちろん関数の外にも見つからなければエラーです。
scala> val a = (x: Int) => x + more
:10: error: not found: value more
val a = (x: Int) => x + more

変数が存在すれば参照可能です。
scala> val more = 10
more: Int = 10

scala> val a = (x: Int) => x + more
a: Int => Int =

関数から見ると、moreは自由変数(free varibles)で、xは束縛変数(bound variables)です。
束縛変数だけ使っている関数リテラルは閉じた項(closed terms)と呼び、自由変数を使っている関数リテラルは開いた項(open terms)と呼びます。

開いた項は、関数リテラルから関数値を作成するときに自由変数の束縛をつかみ、閉じます。そのためその関数値をクロージャー(closure)と呼びます。

クロージャーは自由変数の束縛をつかみます。
つまり値では無く変数をつかむので、クロージャー作成後の変数の変更は参照可能です。
また、クロージャーの中で変数に対して変更を加えると、クロージャーの外にも影響します。

scala> var sum = 0
sum: Int = 0

scala> someNumbers.foreach(sum += _)

scala> sum
res20: Int = 99

連続パラメータ

Javaで言う所の可変長引数です。
関数のパラメータの型の後ろに*を付けることで連続パラメータとなります。

scala> def echo(args: String) =
| for (arg <- args) println(arg)
echo: (args: String
)Unit

scala> echo(“hoge”,”fuga”,”foo”, “bar”)
hoge
fuga
foo
bar

連続パラメータは関数内ではパラメータ型のArrayとなります。

Arrayなのであれば連続パラメータとしてArrayを入力としてできそうですが、そのままではコンパイルエラーです。

scala> def list = List(“1”, “2”, “3”)
list: List[String]

scala> echo(list)
:13: error: type mismatch;
found : List[String]
required: String
echo(list)

パラメータに_*の型を指定すると連続パラメータとして入力可能になります。
scala> echo(list: _*)
1
2
3

名前付き引数

通常関数の引数は、順番通りにパラメータに設定されます。

scala> def func1(x: Int, y: Int) = println(s”x=$x, y=$y”)
func1: (x: Int, y: Int)Unit

scala> func1(10,20)
x=10, y=20

引数を指定する際にパラメータ名も指定すると、その順番を入れ替えることができます。
scala> func1(y = 10, x = 20)
x=20, y=10

名前付き引数は次のパラメータのデフォルト値と一緒に使います。

パラメータのデフォルト値

Scalaではパラメータにデフォルト値を設定することができます。

scala> def func2(x: Int = 10, y: Int = 20) = println(s”x=$x, y=$y”)
func2: (x: Int, y: Int)Unit

scala> func2()
x=10, y=20

一つ引数を指定すると、最初のパラメータのみ引数の値が設定されます。
scala> func2(100)
x=100, y=20

二つ目の引数だけ値を設定したい場合は名前付き引数を使います。
scala> func2(y = 100)
x=10, y=100

末尾再帰

Scalaは末尾再帰の最適化がされます。
末尾再帰の最適化をしない場合はscalaシェル実行時かscalacでのコンパイル時に-g:notailcallsをしていします。

ただ、Scalaの末尾再帰最適化は自分自身の関数を呼び出す際だけにされ、二つの関数を呼び合うような末尾再帰は最適化されません。
また、値としての関数を呼び出す際にも末尾再帰されません。

No Comments

Post a Comment

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