先日に引き続き、
ITProに載ってる Scala 講座の、「
第2回 Scalaの基本的な文法」をやってみる。
今回のポイントは簡単な変数宣言から簡単な関数の定義まで。ハイライトは高階関数とパターンマッチングといかにも関数型言語風の再帰処理。いろいろ疑問も生じるが、そのうちわかるのだろうと、とりあえず最後までやってみる。掲載されているコードは、特に問題なく動作した。
復習のために、今回で紹介されていた事と、検索でたどり着いた他のサイトから得た少々の知識を使って自主練習してみた。以下、分散を求めるコードを、まず再帰とパターンマッチングを使って書く。次に、これを高階関数を用いるパターンに書き直してみる。更に現時点の手持ちの知識で書き直して、いろいろ試してみる。
1.高階関数なし
まず単純にList中の要素の合計を求める sum() 関数を定義してみる
def sum(l:List[Double]):Double = l match {
case Nil => 0
case head::tail => sum(tail) + head
}
各要素について平均値との差の2乗を計算しリストを生成する varianceSub() 関数を以下のように書く(これは後で直す)
def varianceSub(list:List[Double], average:Double):List[Double] = list match {
case Nil => Nil
case head::tail => Math.pow((head - average), 2) :: varianceSub(tail, average)
}
最後に、varianceSub() で求めたリストの平均値を算出して分散を求める variance ()関数を書く
def variance(list:List[Double]) = {
average(varianceSub(list, average(list)))
}
2.高階関数を使ったやつ
上記コードを無名の高階関数を使って書き直してみた。
def sum2(l:List[Double])(f:Double=>Double):Double = l match {
case Nil => 0
case head::tail => sum2(tail)(f) + f(head)
}
def variance2(list:List[Double]) = {
val avg = sum2(list)(elem => elem) / list.length
sum2(list)(elem => Math.pow(elem - avg, 2)) / list.length
}
高階関数ありと無しの二つのコードで、以下のように同じ結果が得られる事が確認できた。
scala> variance(List(46.1, 52, 48.5, 46.5, 46, 44.8, 48.5, 53.4))
res60: Double = 8.169375
scala> variance2(List(46.1, 52, 48.5, 46.5, 46, 44.8, 48.5, 53.4))
res61: Double = 8.169375
3.標準のList操作関数を使ってみる
sum2 のようなのはList操作関数としておそらく標準的に提供されているに違いないと考え、検索してみると、やはりある。そこで variance2() 関数を sum2()を使わないパターンで書き換えてみた。
def variance3(list:List[Double]) = {
val avg = list.foldLeft[Double](0) {(sum, y) => sum + y} / list.length
list.foldLeft[Double](0) {(sum, elem) => sum + Math.pow(elem - avg, 2)} / list.length
}
4.タプルを使ってみる
ここまでのコードは、平均値を算出するときと、差の2乗値 を算出するときの2回往復分リストを走査しているが、本来は、行きで平均値を算出し戻りで差の2乗を出せるはずなので無駄がある。但しこれをやるには、差の2乗と一緒に平均値も同時に関数の戻りとして返す仕組みが必要になる。そこで調べてみると、タプルというものを使って複数の戻り値を返せるというので使ってみた。
まずリストの「先頭→末尾」方向に走査する際に合計を出しておき、末尾(case Nil のところ)で平均値を算出し、これを Tuple の要素1に入れておく。折り返して「末尾→先頭」方向に戻るときに、この平均値を使って差の2乗を算出し、これを足しこんで要素2に保持しておく。
def variance4(l:List[Double], sum:Double, len:Double):Tuple2[Double,Double] = l match {
case Nil => new Tuple2[Double, Double](sum / len, 0)
case head::tail => x(tail, sum + head, len) match {
case Tuple2(avg, sum) => new Tuple2(avg, sum + Math.pow(head - avg, 2))
}
}
最終的に関数が終了したときに返されるのは、差の合計を保持する Tupleオブジェクトなので、ここから分散を得るにはTupleの要素2を取り出して、要素数で割る必要がある。適当に関数を定義してもよいが自明なので省略。下のような結果が得られ、正しく動作している事が確認できた。
scala> aList = List(46.1, 52, 48.5, 46.5, 46, 44.8, 48.5, 53.4)
aList: List[Double] = List(46.1, 52.0, 48.5, 46.5, 46.0, 44.8, 48.5, 53.4)
scala> variance4(aList, 0, aList.length)._2 / aList.length
res103: Double = 8.169374999999999
上記コードのように戻り値を二つ返す際にTupleを使う場合に、毎回 new しているのが気になる。Tuple がイミュータブルにできているようなので、このように書かざるを得なかったが、今後マシなやり方を探してみたい。