日々精進

aikoと旅行とプログラミング

はじめてのScala【コップ本第4章】

4.1 クラス、メソッド、フィールド

クラスを

class ChecksumAccumulator {
  //メソッドやフィールドを定義
}

としたとき、

new ChecksumAccumulator

とすることでインスタンス化することができる。クラス定義の中には、フィールドとメソッドを配置する。(これらはまとめてメンバと呼ばれている)

val acc = new ChecksumAccumulator

としたとき、accへ新たに代入を行うことはできないが、accの参照する先が書き換えられることがある点に注意する必要がある。
このような状況でオブジェクトを堅牢に保つには、フィールドを非公開にしてクラス外からのアクセスを防ぐようにする必要がある。

class ChecksumAccumulator {
  private var sum = 0
}

このようにすることで、外部からの直接なアクセスを防ぐことができる。

val acc = new ChecksumAccumulator
acc.sum = 5 //NG

Scalaでは、明示的にアクセス修飾子を記述しない場合publicとして扱われる。
外部からprivateな変数にアクセスするには、クラス内にメソッドを定義する必要がある。そこで以下のようにaddとchecksumというメソッドを追加する。

class ChecksumAccumulator{
  private var sum = 0
  def add(b: Byte): Unit = {
    sum += b
  }
  def checksum(): Int = {
    return ~(sum & 0xFF) + 1
  }
}

いま追加したaddとchecksumはもう少し簡潔に書くことができる。Scalaではメソッド内で計算された最後の値を返すことになっているので、メソッドchecksumのreturnというワードは不要である。また、メソッドの中身が単一実行分であることから、中括弧を省略すると

class ChecksumAccumulator{
  private var sum = 0
  def add(b: Byte): Unit = sum += b
  def checksum(): Int = ~(sum & 0xFF) + 1
}

また、副作用のあるメソッド(今回はaddが該当)の場合、等号と結果型を省略して中括弧で囲むという方法で記述できるので

class ChecksumAccumulator{
  private var sum = 0
  def add(b: Byte){ sum += b }
  def checksum(): Int = ~(sum & 0xFF) + 1
}

のように記述することができる。
気をつけなければならないのは、関数定義の際に等号を省略してしまうと結果型がUnitになってしまう点である。例えば

def g() { "this String gets lost too" } 

とした時、関数内の最後がString型となっていてもUnit型が返ることになってしまう。上記の関数で結果型をStringとしたい場合は

def g() = { "This String gets lost too" }

とする必要がある。

4.2 セミコロン推論

Scalaでは文の末尾にセミコロンを書く必要はない。ただし、1行に複数の文を書く時はセミコロンが必要となる。

val a = 10; println("a" + a)

ただし、

x
+ y

のような文は、xと+yという2つの文として認識される。このような文は、カッコで囲むことで確実に1文として認識される。

(x
+ y)

カッコを使わずに書く方法として

x + 
y

と書いても1文として認識される。Scalaでは基本的に演算子を行末に揃えて書くべきである。

セミコロン推論の規則
  • 該当行の末尾が、ピリオドや中置演算子など文の末尾として文法的に認められていない単語のとき
  • 次の行が文の先頭として認められていない単語になっている
  • カッコの中にいる状態で文末になっている

いずれかに当てはまらない限り、行末がセミコロンとして認識される。

4.3 シングルトンオブジェクト

ScalaではStaticなメンバがない代わりにシングルトンオブジェクトを持っている。シングルトンオブジェクトはJavaでいうところの静的メソッドの定義場所と考えると良さそう。シングルトンオブジェクトは、classキーワードの代わりにobjectキーワードを使うことで宣言できる。

import scala.collection.mutable.Map

object ChecksumAccumulator {
  private val cache = Map[String, Int]()
  def calculate(s: String): Int = {
    if(cache.contains(s))
      cache(s)
    else{
      val acc = new ChecksumAccumulator
      for(c <- s)
        acc.add(c.toByte)
      val cs = acc.checksum()
      cs
    }
  }
}

ChecksumAccumulatorの前がclassではなくobjectとなっている。先ほど、ChecksumAccumulatorというクラスを定義したのを覚えているだろうか。このようにシングルトンオブジェクトがほかのクラスと同じ名前を持つ時、シングルトンオブジェクトのことをコンパニオンオブジェクトと呼ぶ。またそのクラスのことをコンパニオンクラスとよんでいる。
先程も述べたように、シングルトンオブジェクトをJavaの静的メソッドの定義場所と考えることができるので、次のように呼び出すことができる。

ChecksumAccumulator.calculate("Every Value is an object.")

クラスとシングルトンオブジェクトの違いとして、パラメータを取れるか取れないかという違いがある。クラスはパラメータを取ることができるが、シングルトンオブジェクトはパラメータを取ることが出来ない。
また、コンパニオンクラスと同じ名前を共有しないシングルトンオブジェクトのことを、スタンドアロンオブジェクトという。

4.4 Scalaアプリケーション

Scalaプログラムを実行するには、mainメソッドを持つスタンドアロンシングルトンオブジェクトを作らなければならない。

//Summer.scala
import ChecksumAccumulator.calculate

object Summer{
  def main(args: Array[String]) {
    for (arg <- args){
      println(arg + " : " + calculate(arg))
    }
  }
}

mainメソッドのパラメータはArray[String], 結果型はUnitである。先頭に記述したimportによって、メソッドを単純名で扱えるようにしている。Scalaではjava.langとscalaパッケージのメンバ、Predefの3つを暗黙的にインポートしている。
Javaでは公開クラスはクラスと同じ名前のファイル名にしなければならなかったが、Scalaではその規則に従う必要はない。ただし、一般的には作成したクラスとファイルの名前は一緒にするスタイルが推奨されている。
上記のファイルを実行するには

$ scalac ChecksumAccumulator.scala Summer.scala

とし、

$ scala Summer of love

とすることで、

of : -213
love : -182

と出力される。もしくは、fsc(fast Scala compiler)というコンパイラデーモンを使うとより高速に実行できる。

$ fsc ChecksumAccumulator.scala Summer.scala