LL脳がscalaの勉強を始めたよ その25

Scalaコップ本の8章の続きをやりますよー、今回は連続パラメーターからですなー

連続パラメーター

Scalaは関数の最後の引数として可変長引数(連続パラメーター)が渡せますよー、という話デス

可変長引数を使うには型指定の後ろに*をつけますよー

// 引数の型指定に*をつけて可変長引数を定義します
scala> def echo(args:String*) = for(arg <- args) println(arg)
echo: (String*)Unit

//// 実際に可変長の引数を渡してみますよー
// 引数なしだと実行されません
scala> echo()

// 引数1個付けてみます
scala> echo("hoge")
hoge
// 引数2個付けてみます
scala> echo("hoge", "huga")
hoge
huga
// 引数3個付けてみます
scala> echo("hoge", "huga", "moge")
hoge
huga
moge

いや、これ便利だわ。PHPでもfunc_get_argsで取得できたけど、関数式に組み込んであると使いやすいっすねー。可変長の引数と単独引数を混ぜて使えるし。

ちなみにパラメータの最後以外で呼び出すと怒られますな、まあ何が可変長か分からなくなるから当然でしょうけども(´・ω・`)

scala> def echos(args:String*, n:Int) = for(arg <- args) println(arg) 
<console>:4: error: *-parameter must come last
       def echos(args:String*, n:Int) = for(arg <- args) println(arg)
                 ^
連続パラメーターはArray型なんだけども

関数内では連続パラメーターの型はArray型になってるみたいデス。でもこういうのは通らないの(´・ω・`)

// Array型で引数を渡してやるぜ
scala> echo(Array("hoge", "huga"))
// 型がチゲーだろゴルゥア(# ゚Д゚)と怒られました
<console>:6: error: type mismatch;
 found   : Array[java.lang.String]
 required: String
       echo(Array("hoge", "huga"))
            ^

内部的にArray型を使っているものの可変長引数のための受け口なので受付けることができないみたいです。

どうしてもArrayで渡したいんだc(`Д´と⌒c)つ彡 ヤダヤダ c(,Д、と⌒c)つ彡 ジタバタという場合は、配列引数の後ろに": _*"を付与します。

// コンパイラに配列引数であることを明示しますよー
scala> echo(Array("hoge", "huga"): _*)
hoge
huga

配列引数であることを明示して渡すことでコンパイラ様がうまい具合に引数処理してくれるようです。

末尾再帰

関数型を推奨しているScalaではvarを(極力)使わないのでループを再帰関数でおきかえることがよくあるけども、実行速度が落ちない(最適化される)再帰とされない再帰があるから注意(*´・ω・)(・ω・`*)ネーという話です。

結論からいうと末尾再帰という再帰を使う場合は、命令型ループに比べても速度が落ちないらしいです。末尾再帰とは関数処理の最後で自分自身を呼び出しているような再帰関数デス

それでは末尾再帰のサンプルを書いてみますよー

// 末尾で自分自身を呼びださいているので末尾再帰です
scala> def recPrint(i:Int):Int = if(10 < i) i else recPrint(i + 1)
recPrint: (Int)Int
// 実行結果ですよー
scala> recPrint(1)                                                
res0: Int = 11

似た様な処理の命令型ループ式を書いてみますよー

// varとループで定義しマス
scala> def recPrintLoop(i:Int):Int = { 
     |   var n = i
     |   while(n < 11) n += 1
     |   n
     | }
recPrintLoop: (Int)Int
// 実行結果も同じですなー
scala> recPrintLoop(1)                 
res0: Int = 11

上で書いた2つの末尾再帰式とループ式は、コンパイラ的には同じものとして解釈するそうデス。そのため実行時のオーバーヘッドを気にせずにじゃんじゃん末尾再帰を使うとイイヨ!とのことですね。まあ、コードが簡潔になるので

ちなみにコップ本ではコンパイル後のJavaバイトコードを引っ張り出して比較しておりますな。むむ、まだよくわかんないので修業が必要ですけども(´・ω・`)

末尾再帰にならない例

まずは関数の末尾が自分自身にならない例はだめデス

例えば下のように自分自身の後ろで演算とかしてても末尾再帰じゃないとみなされマス

// 末尾で自分自身に1を加える演算をしているのでダメダメです
def recPrint(i:Int):Int = if(10 < 1) i else recPrint(i + 1) + 1

また、自分自身を別変数に置き換えて使用してもダメらしいです

// プレースホルダーを使って再帰関数を格納
val recCopy = recPrint _
// 変数に格納した関数を利用して再帰
def recPrint(i:Int):Int = if(10 < 1) i else recCopy(i + 1)

どうもJava仮想マシンの命令セットだと複雑な末尾再帰を実装するのが難しいので、Scalaでの末尾再帰はかなり制限されているよ、とのことです。

純粋に自分自身を末尾で呼び出さないとダメってことですねー、変わったことすんな、とc⌒っ゚д゚)っφ メモメモ...

末尾再帰関数をトレースする

末尾再帰関数は呼び出しの度にスタックフレームを作らないらしい…というのを実際に確かめてみますよー

ちなみにスタックフレームってのは、こんな感じらしい

関数呼び出しの際に使われた関数の引数や関数で定義された変数を一時的に記憶するためのメモリ領域

ああ、メモリ領域ね、メモリ領域(゚∀゚) (いい加減になんとなく理解した気になってみる)

うん、とりあえずスタックフレームを大量生産しなければメモリが節約できて効率的です!ってことでいいのかしら?

まあ、とりあえずサンプルやってみましょうか


どうやら下のようなコードでトレースできるんみたいなんだけど、なんでトレースできるのかが謎デス。throw newで失敗条件を発行して、失敗時のプログラムスタックトレースを出力しているっていう解釈でいいかしら?

// 末尾再帰関数をトレースしますよ
def boom(x:Int):Int = {                  
   if(x == 0) throw new Exception("boom!")
   else boom(x - 1)                       
}

scala> boom(3)                                  
java.lang.Exception: boom!
	at .boom(<console>:6)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingM...

at .boom(:6)がスタックフレームの箇所っぽいですね、確かに1個だけみたいです。

そんで末尾再帰じゃなくなるとこんな感じですねー

// 末尾再帰じゃないものをトレースしますよ
def boom(x:Int):Int = {                  
   if(x == 0) throw new Exception("boom!")
   else boom(x - 1) + 1
}

scala> boom(3)
java.lang.Exception: boom!
	at .boom(<console>:5)
	at .boom(<console>:6)
	at .boom(<console>:6)
	at .boom(<console>:6)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl...


スタックフレームを4回作っているみたいだなぁ。再帰も4回呼び出しなので、再帰が呼び出されるたびにスタックフレーム作っているっぽいですねー

たぶんthrow newでエラー発行しているっぽいなぁ

うーん、こうするとトレースできないからthrow newを定義するとトレースになるのかしらん?あとで調べてみよう。

def boom(x:Int):Int = {                  
   if(x == 0) x
   else boom(x - 1)                       
}
スタックトレースオプション

scalaシェルやscalacに-g:notailcallsを付けると末尾再帰関数の最適化をとめられるみたいですねー

んじゃ、勢いでやってみますねー。以下の内容をboom.scalaとして保存します

// 末尾再帰関数を定義
def boom(x:Int):Int = {                                                                                       
   if(x == 0) throw new Exception("boom!")
   else boom(x - 1)
}
// 実行しますよ
boom(3)

オプション付けて実行しますよー

// 最適化しないオプションをつけて実行してみますよ
// 確かにスタックフレームが呼び出されるたびに作成されておりますな
% scala -g:notailcalls boom.scala
java.lang.Exception: boom!
	at Main$$anon$1.boom((virtual file):6)
	at Main$$anon$1.boom((virtual file):7)
	at Main$$anon$1.boom((virtual file):7)
	at Main$$anon$1.boom((virtual file):7)
	at Main$$anon$1.<init>((virtual file):10)
	at Main$.main((virtual file):4)
	at Main.main((virtual file))
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:616)
	at scala.tools.nsc.ObjectRunner$$anonfun$run$1.apply(ObjectRunner.scala:75)
	at scala.tools.nsc.ObjectRunner$.withContextClassLoader(ObjectRunner.scala:49)
	at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:74)
	at scala.tools.nsc.ScriptRunner$.scala$tools$nsc$ScriptRunner$$runCompiled(ScriptRunner.scala:381)
	at scala.tools.nsc.ScriptRunner$$anonfun$runScript$1.apply(ScriptRunner.scala:414)
	at scala.tools.nsc.ScriptRunner$$anonfun$runScript$1.apply(ScriptRunner.scala:413)
	at scala.tools.nsc.ScriptRunner$.withCompiledScript(ScriptRunner.scala:351)
	at scala.tools.nsc.ScriptRunner$.runScript(ScriptRunner.scala:413)
	at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:168)
	at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

// ちなみに最適化済みだとこんな感じです
// スタックフレームの作成は1回だけですな
% scala boom.scala
java.lang.Exception: boom!
	at Main$$anon$1.boom((virtual file):6)
	at Main$$anon$1.<init>((virtual file):10)
	at Main$.main((virtual file):4)
	at Main.main((virtual file))
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:616)
	at scala.tools.nsc.ObjectRunner$$anonfun$run$1.apply(ObjectRunner.scala:75)
	at scala.tools.nsc.ObjectRunner$.withContextClassLoader(ObjectRunner.scala:49)
	at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:74)
	at scala.tools.nsc.ScriptRunner$.scala$tools$nsc$ScriptRunner$$runCompiled(ScriptRunner.scala:381)
	at scala.tools.nsc.ScriptRunner$$anonfun$runScript$1.apply(ScriptRunner.scala:414)
	at scala.tools.nsc.ScriptRunner$$anonfun$runScript$1.apply(ScriptRunner.scala:413)
	at scala.tools.nsc.ScriptRunner$.withCompiledScript(ScriptRunner.scala:351)
	at scala.tools.nsc.ScriptRunner$.runScript(ScriptRunner.scala:413)
	at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:168)
	at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

うーんJavaの世界のエラーメッセージをほんのちょっと理解できたような気がする…じっくりがんばろうっと…

以上ー

ようやく8章が終わったので、次回からは9章やりますよー。カリー化とかか…インド?