あるプログラマの日記

プログラマのメモ、出来事、考えたこと、勉強とかの雑記

再帰

関数型言語では、副作用のないプログラムを行うため繰り返し処理はループではなく再帰を使用。
ただ、Scala で限定的に使用するループで副作用(変数の更新)があっても特に問題なさそうだが、やはりできるだけ副作用のない再帰を使いたい。
再帰のサンプルは,リスト要素の整数値を合算する再帰関数
リストのパターンマッチで Nil(空)の場合は加算する要素がないので、acc をそのまま返す。
パターンマッチでリストのhead(先頭要素) と tail(先頭以外の要素のリスト) の形式になる場合は
acc にリストの先頭要素を足した値とtail部分を引数として自分自身を再帰呼び出ししている。

import scala.annotation.tailrec

object Test1 {
  @tailrec
  def sum(acc: Int, list: List[Int]): Int = {
    print(acc + " ")
    list match {
      case Nil => acc
      case e :: es => sum(acc + e, es)
    }
  }
  
  def main(args: Array[String]) = {
    val list = List(1, 2, 3, 4, 5, 6, 7)
    val result = sum(list.head, list.tail)
    println("result: " + result)
  }
}

sumメソッドの引数 acc は、アキュムレータと呼ばれ、再帰関数の途中の計算結果の値を保持する。
アキュムレータを使用するのは関数の最後で再帰的に自分自身を呼び出して末尾再帰にするため。
末尾再帰コンパイル時に最適化されて命令型コードと同等の形に変換される。
なので、リストの要素数が増えてもスタックオーバーフローは発生しない。
Scala 2.8 から再帰関数に @tailrec アノテーションをつけると末尾再帰でない場合に
コンパイルエラーにしてくれる。

実行結果

$ scala Test1
1 3 6 10 15 21 28 result: 28

次は、アキュムレータを使用していない末尾再帰でない例
sumメソッドの再帰呼び出し部分は、e + sum(es) になっているため
再帰呼び出しが最後ではなく再帰呼出し後に e と加算処理を行う。

import scala.annotation.tailrec

object Test2 {
  @tailrec
  def sum(list: List[Int]): Int = {
    list match {
      case Nil => 0
      case e :: es => e + sum(es)
    }
  }
  
  def main(args: Array[String]) = {
    val list = List(1, 2, 3, 4, 5, 6, 7)
    val result = sum(list)
    println("result: " + result)
  }
}

@tailrec アノテーションを付けているのでコンパイルするとsumメソッドが
末尾再帰でないためコンパイルエラーになる。
@tailrec アノテーションをとっておくとコンパイルは通り
リストの要素数がある程度少なければ動作するが、リストの要素数
増えるとスタックオーバーフロー(java.lang.StackOverflowError)が発生する。

$ scalac test2.scala
test2.scala:8: error: could not optimize @tailrec annotated method sum: it contains a recursive call not in tail position
      case e :: es => e + sum(es)
                        ^

list のチェックは .. match { case .. => .. } 式のパターンマッチを使用。
case Nil は空のリストのケースで、case e :: es *1 は e が list.head で es が list.tail になりリストが先頭要素とそれ以外の要素のリストの形式になる場合をあらわしている。

list match {
  case Nil => acc
  case e :: es => sum(acc + e, es)
}

if (list.isEmpty) acc else sum(acc + list.head, list.tail)

と同じ処理になる。
上の if .. else .. 式に書き換えた Test1 オブジェクト

import scala.annotation.tailrec

object Test1 {
  @tailrec
  def sum(acc: Int, list: List[Int]): Int = {
    print(acc + " ")
    if (list.isEmpty) acc else sum(acc + list.head, list.tail)
  }

  def main(args: Array[String]) = {
    val list = List(1, 2, 3, 4, 5, 6, 7)
    val result = sum(list.head, list.tail)
    println("result: " + result)
  }
}

末尾再帰が最適化される条件

  • 自分自身を呼び出している末尾再帰関数
  • 再帰関数がオーバーライドで変更されないメソッド(関数) *2

*1: ::(e, es) と同じ

*2:メソッドが final か private か、または メソッドのクラスが final

関数型言語を採用する時の壁

副作用のない処理、パターンマッチ、ケースクラス、ファーストクラス関数、高階関数クロージャ、カリー化、関数部分適用、末尾再帰、アクター、並列処理、型推論.. それぞれが関連しあって簡潔なコードが書けて、副作用によるバグも減少できるメリットがあるのですが、現場では、なかなか関数型言語へ移行できない壁があったりします。
ネックになるところは

関数型言語導入のとっかかりとして

というような感じで徐々に現場で認知してもらって、
関数型言語へフェードインできると良いのですが..

そして、関数型言語の勉強をちまちまと継続

暗黙の引数 (implicit parameters)

引数の型に合わせた暗黙の値を指定しておくと、関数(メソッド)呼び出し時の引数を省略できる。
関数定義の引数リストで引数名の前に implicit を付けると暗黙の引数が適用される。
引数を省略しないときは関数呼び出し時の指定値がそのまま渡される。
implicit は引数リスト内の先頭の引数以外には指定できないが、これは個別の引数への指定ではなく
引数リスト全体に適用されている。
デフォルト引数によく似ているが、デフォルト引数は、関数の定義でデフォルト値を指定する
のに対して暗黙の引数は呼び出し元で引き数の暗黙の値を定義する。
例では、一般的な Int と Long にマッチさせる暗黙の引数を定義しているが、通常は個別に型を
作って偶然の型の一致が起こらないようにするのが良さそうである。

scala> def foo(a: Int, implicit b: Long): Long = a * b
<console>:1: error: identifier expected but 'implicit' found.
       def foo(a: Int, implicit b: Int) = a * b
                       ^
scala> def foo(implicit a: Int, b: Long): Long = a * b
foo: (implicit a: Int, implicit b: Long)Long

scala> implicit val defValue = 10
defValue: Int = 10

scala> implicit val defLValue = 20L
defLValue: Long = 20

scala> foo
res1: Long = 200

scala> foo(3, 4)
res2: Long = 12

複数の引数リストをとる関数の最後尾の引数リスト以外に暗黙の引数は適用できない。

scala> def bar(a: Short)(implicit b: Long, c: Int): Long = a + b * c
bar: (a: Short)(implicit b: Long, implicit c: Int)Long

scala> bar(5)
res3: Long = 205

scala> def foo2(implicit a: Int)(b: Int): Int = a * b
<console>:1: error: '=' expected but '(' found.
       def foo2(implicit a: Int)(b: Int): Int = a * b
                                ^

scala> def foo3(a: Int)(implicit b: Long)(c: Short): Long = a * b + c
<console>:1: error: '=' expected but '(' found.
       def foo3(a: Int)(implicit b: Long)(c: Short): Long = a * b + c
                                         ^

scala> def foo4(implicit a: Int)(implicit b: Long): Long = a * b
<console>:1: error: '=' expected but '(' found.
       def foo4(implicit a: Int)(implicit b: Long): Long = a * b
                                ^

呼び出した関数で適用した暗黙の引数の型の implicit val 定義が
スコープ内になければエラーになる。

scala> def bar2(a: Int)(implicit b: Long, c: Short): Long = a + b * c
bar7: (a: Int)(implicit b: Long, implicit c: Short)Long

scala> bar2(7)
<console>:11: error: could not find implicit value for parameter c: Short
       bar2(7)

scala> implicit val defSValue: Short = 5
defSValue: Short = 5

scala> bar2(7)
res8: Long = 107

呼び出した関数で適用した暗黙の引数の型のimplicit val の定義がスコープ内で
2つ以上ある場合もエラーになる。

scala> implicit val defs2: Short = 3
defs2: Short = 3

scala> bar2(7)
<console>:13: error: ambiguous implicit values:
 both value defSValue in object $iw of type => Short
 and value defs2 in object $iw of type => Short
 match expected type Short
       bar2(7)
           ^

あるスコープで、型に定義している暗黙の値を確認するには Predef の implicitly[型] メソッドを使う。

scala> implicitly[Int]
res10: Int = 10

scala> implicitly[Long]
res11: Long = 20

Predef の implicitly の 定義

def implicitly[T](implicit e: T): T = e

桜満開ですが、明日は雨

最近、早起きできていなかったので、ひさびさに早起きした。

仕事は少しだけ中だるみ状態。

Scala の勉強をぼちぼちと進めているので、仕事で実用的に使える機会を

なんとかつくりたいがなかなか難しい。

あっ、こんな時間、出勤しなくては..

あと、関数型プログラミングでなにがうれしいのかということを
整理するのに、Scala のメリットというか、関数型プログラミングのメリットについて
以下まとまっていそうだったので貼りました。

関数型言語で工数削減できる理由、前編

関数型言語で工数削減できる理由、後編

かなり以前にかかれた少し小難しい論文で
なぜ関数プログラミングは重要か
というのもありました。

Stream

Listとほぼ同じで、違いは実際に要素が指定されるまで作成が遅延される。
要素が無限に続くリストや要素数を事前に決められないリストとして表現できる。

Stream.cons は List の :: と同じように 先頭要素とリスト(Stream)を連結する。
cons は Streamコンパニオンオブジェクトのオブジェクト。

scala> def from(start: Int): Stream[Int] = Stream.cons(start, from(start + 1))
foo: (start: Int)Stream[Int]

scala> from(1).take(3).foreach(println)
1
2
3

最初に要素を指定して作成することもできる。*1
contains で要素の有無を確認できるが、無限リストの状態では、まだ追加していない要素を contains すると結果が返されないのでやってはダメ*2。放置すると OutOfMemoryError が発生。

scala> val bar = Stream(3, 4, 5)
bar: scala.collection.immutable.Stream[Int] = Stream(3, ?)

scala> bar(1)
res2: Int = 4

scala> bar(2)
res3: Int = 5

scala> bar.contains(3)
res4: Boolean = true

scala> bar.contains(2)
res5: Boolean = false

scala> val bar2 = from(5)
bar6: Stream[Int] = Stream(5, ?)

scala> bar2.contains(0)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2882)
	at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
..snip..

Stream.cons は、Streamコンパニオンオブジェクトの #:: オブジェクトを使って書くことも可能

scala> Stream.cons(1, Stream.cons(2, Stream.empty)).print
1, 2, empty

scala> (1 #:: 2 #:: Stream.empty).print
1, 2, empty

*1:これは、Stream を使用する意味があまりないような...

*2:当然といえば当然

遅延評価

名前渡し引数(by-name parameter)

名前渡し引数は、メソッド内で引数の名前が実際に使用される時に評価される。
引数が複数回参照される場合は、参照される度に評価される。
引数の型の前に => を付ける。(引数名: => 型)
Scala は通常は、値渡し(関数を呼び出す前に引数が評価される)

scala> def foo(f: => Unit) = { print("head "); f; println(" tail") }
foo: (f: => Unit)Unit

scala> foo(print("foobar"))
head foobar tail

scala> def foo2(f: => Int) = { print("head "); print(f); println(" tail") }
foo2: (f: => Int)Unit

scala> val a = 3
a: Int = 3

scala> val bar = (b: Int) => { print(b); b * 2 }
bar: (Int) => Int = <function1>

scala> foo2(bar(a))
head 36 tail

scala> foo2(a)
head 3 tail

lazy val

val で宣言するフィールドまたはローカル変数の val の前に lazy を指定すると
実際に変数が使用される時に初めて評価されて初期化される。

scala> lazy val x1 = { println("init x1"); 1 }
x1: Int = <lazy>

scala> val x2 = { println("init x2"); 2 }
init x2
x2: Int = 2

scala> x1
init x1
res5: Int = 1

scala> x1
res6: Int = 1

実際に必要となった時に初めて評価されて変数の初期化は1度しか行われない。

デフォルト引数、名前付き引数

デフォルト引数

関数、メソッドの定義で引数にデフォルト値を指定できる。

  • デフォルト引数は引数の一部またはすべてに指定できる。
  • デフォルト引数は、関数、メソッドの定義の際に 引数名: 型 = デフォルト値 の形式で定義する。
  • 可変長引数と併用することはできない。
  • 呼び出し時に引数を省略すると定義したデフォルト値が使用される。
  • 引数全てにデフォルト値が定義されている場合で、全ての引数を省略してメソッドを呼び出す場合 () は省略できない。
scala> def foo(list: List[String], s: String = ",") = list.mkString(s)
foo: (list: List[String], s: String)String

scala>  val list = List("foo", "bar", "hoge")
list: List[java.lang.String] = List(foo, bar, hoge)

scala> foo(list)
res0: String = foo,bar,hoge

scala> foo(list, "-")
res1: String = foo-bar-hoge

scala> def foobar(a: String = "foo", b: String = "bar") = a + b
foobar: (a: String, b: String)java.lang.String

scala> foobar
<console>:9: error: missing arguments for method foobar in object $iw;
follow this method with `_' if you want to treat it as a partially applied function
       foobar
       ^

scala> foobar()
res3: java.lang.String = foobar

名前付き引数

関数、メソッドの呼び出し時に引数名を指定して呼び出せる。

  • 呼び出し時に 引数名 = 値 の形式で指定する。
  • 名前付き引数で呼び出す場合は任意の順番に変えることができる。
  • 引数が多い場合は、名前付き引数を使用すると可読性が良くなる。
  • デフォルト引数と名前付き引数を組み合わせると引数の数の違いによるオーバーロードが不要になり可読性も良くなる。
scala> def bar(year: Int = 2012, month: Int = 4, day: Int= 7) = "%04d/%02d/%02d".format(year, month, day)
bar: (year: Int, month: Int, day: Int)String

scala> bar(month = 5, day = 5)
res4: String = 2012/05/05

scala> bar()
res5: String = 2012/04/07

scala> bar(day = 10)
res6: String = 2012/04/10

scala>  bar(month = 8)
res7: String = 2012/08/07

scala> bar(month = 7, day = 1, year = 2014)
res8: String = 2014/07/01

scala>  bar(2014, month = 7)
res9: String = 2014/07/07

scala>  bar(2014, month = 7, 2)
res10: String = 2014/07/02