読者です 読者をやめる 読者になる 読者になる

scalaの魅力に取り憑かれ気味

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版


アリ本が読み終わったので、Scalaスケーラブルプログラミングを読んでます。いわゆるコップ本。

非常に内容の濃い本なので全て読み終わるのには時間がかかりそうです。
今のところ第8章まで読みました。

ScalaJava VM上で動作する関数型プログラミング言語です。
関数型、といっても手続き型の書き方もできるハイブリッド型言語ですね。
関数型と手続き型の良いところ取りをしつつ、Javaの豊富なライブラリにもアクセスでき、
DSLのように自分で直感的な文法で記述することもできるという個人的にかなりキている言語だと思っています。
なんというか、プリウスみたいなハイブリッド車なのに、4WDみたいなパワーがあるようなイメージ・・・。
※「7つの言語、7つの世界」という本では、Scalaシザーハンズのエドワードに例えられていました。

今回は8章まで読んでて感動 + 少し失望した箇所があったので紹介します。

Scalaは先に述べた通り、手続き型の書き方も関数型の書き方もできます。
ただ関数型の書き方の方を推奨しています。その方が抽象度が高く、コードの行数を減らせるからです。
行数が減らせればその分エラー混入する割合が減ります。

手続き型での書き方

class MultiTable {
val max = 9
def printMultiTable() {
  var i = 1
  while (i <= max) {
    var j = 1
    while (j <= max) {
      val prod = (i * j).toString
      var k = prod.length
      while (k < 10) {
        print(" ")
        k += 1
      }
      print(prod)
      j += 1
    }
    println()
    i += 1
  }
}
}

object MultiTable extends App {
  new MultiTable().printMultiTable
}

このプログラムは掛け算九九をコンソールに出力するプログラムです。
Scala本の第7章に書かれています。
出力結果は以下のようになります。

         1         2         3         4         5         6         7         8         9
         2         4         6         8        10        12        14        16        18
         3         6         9        12        15        18        21        24        27
         4         8        12        16        20        24        28        32        36
         5        10        15        20        25        30        35        40        45
         6        12        18        24        30        36        42        48        54
         7        14        21        28        35        42        49        56        63
         8        16        24        32        40        48        56        64        72
         9        18        27        36        45        54        63        72        81

これは手続き型の書き方です。Java, Cを最初の言語として育った自分には馴染み深いものです。

関数型の場合は以下の通りです。

関数型の場合

class MultiTable {
  val range = 1 to 9
  def makeRowSeq(row: Int) =
  for (col <- range) yield {
    val prod = (row * col).toString()
    val padding = " " * (10 - prod.length)
    padding + prod
  }
  def makeRow(row: Int) = makeRowSeq(row).mkString
  def multiTable() = {
    val tableSeq =
      for (row <- range)
        yield makeRow(row)
    tableSeq.mkString("\n")
  }
}

object MultiTable extends App {
  println(new MultiTable().multiTable())
}

makeRowSeqで1行分の配列(1, 2, 3, 4,... )を作ります。
makeRowで文字列化し、"1 2 3 4 ..."
multiTableで各行分の文字列を作成して返します。
上記に書いたとおり、行数が少しですが減っています。
リスト操作で一気に処理をしているような印象を与えます。

もう一つ特徴があり、varではなく、valで宣言していること、while文を使っていないことなどが
挙げられます。varではなく、valで宣言しているのは関数内で変数が変更されるという影響を減らす為です。
変数が変わるという事はその時点で動作を推測しにくくなります。変数が変更されない事のほうが脳のメモリーは少なく済みます。

同じようにwhile文も使っていません。その代わりfor文を多用しています。for文の方がyield句等関数型言語に使える表現が多いです。

scalaの感動した箇所


このように関数型スタイルで書くと今までのwhile文での繰り返しがなくなります。
別な例として、最大公約数を求める関数の例がありました。
手続き型と関数型のスタイルを以下に出しておきます。
■手続き型

def gcdLoop(x: Long, y: Long): Long = {
  var a = x
  var b = y
  while (a != 0) {
    val temp = a
    a = b % a
    b = temp
  }
  b
}

■関数型

def gcd(x: Long, y: Long): Long =
  if (y == 0) x else gcd(y, x % y)

関数型はwhileループもvarも使っていませんね。その代わり再帰呼び出しで書かれています。
Javaで再帰呼び出しで書くとメソッドコールがスタックに積まれていくため、非常にコスト高なイメージがあります。
Scalaはここがかなり改良されていて、Scalaコンパイラは末尾に再帰呼び出しを検知すると最適化して
関数の冒頭にJUMPするようなコードに変更してくれます。

この様にすることで再帰呼び出しのコストを下げています。
ここが少し感動しました。

がっかりしたところ


上の仕組みは素晴らしいと思ったのですが、
末尾じゃないと使えません、また以下のような場合は使えません。

1. 互いに呼び出し合う関数の場合

def isEven(x: Int): Boolean = 
  if (x==0) true else isOdd(x-1)
def isOdd(x: Int): Boolean =
  if (x==0) false else isEven(x-1)

isEvenとisOddを互いに呼び出して再帰呼び出ししています。
この場合は最適化されません。

2. 値としての関数を使っている場合

val funValue = nestedFun _
def nestedFun(x: Int) {
  if (x != 0) { println(x); funValue(x - 1)}
}

変数のfunValueの方を呼び出している場合ですね。

という訳で再帰呼び出しはコスト高なのは変わりなく、上のやり方はアンチパターンとして
考えておく必要があります。
もう少し頑張って欲しかったなぁと、このままでは関数型で実装したけどなんか遅い、、、
とかありそうで。。。

まとめ


Scalaは面白いです。かなり魅力的な機能があって手続き型では数行書かないといけないところを
1行で書けるとかプログラマにとって気持ちのいい機能が多いです。
ただし、性能面はどうなるか分かりません。
まだ生まれて数歳の言語なので今後の改善を望みます。