新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Scala逆引きレシピ

参考書籍

Scala逆引きレシピ (PROGRAMMER’S RECiPE)

Scala逆引きレシピ (PROGRAMMER’S RECiPE)

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)は3章からScalaがわからないと辛い感じだったのでこちらを先に。

内容

第2章:Scalaの基本

Predef

scala.Predef=Scalaプログラムにおいて暗黙的にインポートされるオブジェクト。
主なメソッドは以下。

  • アサーション
    • assert/assume/require:引数がfalseの場合にAssertionError/IllegalArgumentErrorをスロー
  • 入出力系
    • print/println:引数で指定したオブジェクトをtoString()した文字列をコンソールに出力
    • printf:第2引数以降を第1引数のtextにフォーマットしてコンソールに出力
    • readXxx:コンソールからそれぞれの型の入力を受け取る
    • readLine:メッセージを指定してコンソールから1行分の文字列を受け取る

アサーションのためのメソッドの使い方は以下の通り。

def printMessage(message: String): Unit = {
  assert(message != null)

  println(message)
}

上記のprintMessageにnullを渡すと、AssertionErrorがスローされる。
また、第二引数にメッセージを指定することも可能。
同様のメソッドにはassumeやrequireがあるが、期待値のチェックにはassumeを、引数のチェックにはrequireといった使い分けをすると良い。

パッケージ・インポート

パッケージの定義にはpackageキーワードを使う。

// ソースファイルの先頭でパッケージを定義
package jp.co.hoge

class hogehoge {
...
}
// package宣言を複数に分けて書いても良い
package jp.co
package hoge

class hogehoge {
...
}
// C#の名前空間の定義のように適用範囲を{}で囲むことも
package jp.co{
  package hoge {

    class hogehoge {
      ...
    }
  }
}

インポートにはJavaと同様にimportを使う。Javaよりも柔軟な記述が可能。

import java.util  // パッケージのインポート。util.Listなどの表記が使える
import java.util.ArrayList  // クラス名を指定してインポート。ArrayListの記述が使える
import java.sql.{Date => SqlDate} // 別名を付けてインポート。これでSqlDateとして使える
import java.io._ // パッケージのメンバーを全てインポート。Javaでいうimport hoge.*
import java.lang.Math._ // シングルトンのメンバーをインポート。absなどのメソッドが使える
import java.lang.Math.abs // シングルトンの特定のメソッドをインポート。

// スコープ内でのインポート
def hoge(): Unit = {
  import java.util.Calendar // このメソッド内でのみ有効
}
変数の宣言と遅延評価

変数の宣言にはvar(再代入可能)またはval(再代入不可)を使う。
関数型プログラミングの思想としては、valやイミュータブルなコレクションを使うべき。

また、lazy valで変数やフィールドを宣言することで、その変数やフィールドが実際に参照されるまで評価を遅らせることができる。

var num = 3
lazy val price = num * 100 // ここでは計算されない

// ここでpriceの計算が行われる
println(price)  // => 300
Scalaの型階層

Javaの型はintなどのプリミティブ型と参照型に大別され、すべての参照型はjava.lang.Objectを継承している。
Scalaではすべてがオブジェクトとして実装されており、すべての型はscala.Anyクラスを継承する。

  • Any
    • AnyVal:Javaのプリミティブ型に対応する型のスーパークラス
      • Byte
      • Short
      • Int
      • Unit
      • etc...
    • AnyRef:参照型のスーパークラスで、java.lang.Objectの別名。ユーザ定義クラスはすべてAnyRefを継承
      • java.lang.String
      • プリミティブ型以外のクラス

なお、AnyValのサブクラスであるscala.UnitはJavaのvoidに対応するものだが、実際には()というインスタンスを持つ。
よって、Unit型の変数を定義し、戻り値がUnit型のメソッドの戻り値を格納する(()が入る)ということも(意味はないけど)可能。

特殊な型としてはscala.Nullとscala.Nothingがある。
Nullはnullの型で、参照型であるAnyRefにのみ代入可能である(Javaと同じ)。
Nothingはより特殊な型で、すべての型のサブクラスとして動作するが、値は存在しない
主に異常時に呼び出されるようなメソッドの戻り値として使用する。
(異常時に呼び出されるerrorのようなメソッドの戻り値をUnit型にしてしまうと、
正常時にStringを返すようなメソッド内では戻り値の型が異なる=使えない)

型推論

Scalaでは、ケースによっては変数やメソッドなどの定義において型の記述を省略できる。

// 変数の型を省略
val str = "文字列"
val list = List("A", "B")
// メソッドの戻り値の型を省略
def hello(name: String) = "Hello, %s".format(name)

ただし、型推論によって正しく型付けを行うには、メソッドの内部のすべての分岐から同じ型の値を返すようにする必要がある、
たとえば、条件式によってIntかStringのいずれかを返すメソッドの戻り値の型を省略した場合、戻り値の型はAnyになる。
また、以下のケースでは型を省略できない。

暗黙の型変換

型の変換を行うためのメソッドをimplicitを付けて定義することで、自動的に変換が行われるようにすることができる。

// Date型からCalendar型の変換メソッド
implicit def date2calendar(date: Date): Calendar = {
  ...
}

val date: Date = new Date()
// Calendar型の変数にDate型のオブジェクトを代入できる
val cal: Calendar = date
文字列

複数行の文字列リテラルを記述する場合、ダブルクォート3つで囲む。
また、| を記述してstripMarginを呼び出すことで各行のインデントを合わせることも可能。

val sql = """ |SELECT
                | USER_ID
                |FROM USERS""".stripMargin

文字列の比較には==もしくは!=メソッドを使用する。
Javaでは==演算子は参照の比較だが、Sacalaでは値の比較になる。
 また、Javaと同様にequalsメソッドを使用して参照の比較を行うことも可能。
 なお、参照の比較にはeqメソッドを使用する。)

制御構造

for式は以下のように記述する。

for (i <- 1 to 10) {
  ...
}

// 任意の条件でフィルタリングできる
for (lang <- list if !lang.startWith("J")) {
  ...
}

// 入れ子のforループを簡略化して書ける
for (x <- 1 to 10; y <- x to 10) { ... }
for { x <- 1 to 10
        y <- x to 10} { ... }

なお、while文も記述可能だが、whileループでは判定条件にvarで記述した変数を使う場合が多く、
副作用を持たないという関数型言語らしいコードにならないためあまり利用されない。

パターンマッチ

switch文にあたる機能として、match式がある。

val lang = "Java"

lang match {
  case "Java" => { ... }
  case "Scala" => { ... }
  case _ => { ... }
}

match式では上から順番に判定が行われ、どれかにマッチした場合それ以降の条件に対する判定は行われない。
また、どの条件にもマッチしない場合scala.MatchErrorがスローされるため、全てのパターンを網羅する条件を記述する必要がある。
Javaのdefaultブロックにあたる記述として、_(アンダースコア)がどのような条件にもあてはまるワイルドカードとして機能する。

他にも、型によるパターンマッチや、List・配列・タプル・関数の引数によってパターンマッチを行うことも可能。

エラー処理

scala.Optionは値があるかどうかわからない状態を表すための型で、サブクラスにSomeとNoneがある。
値がある場合にはその値をラップしたSomeを、無い場合はNoneを用いることで型で値の有無を表現できる。
Javaではこのようなケースにはnullを使うのが一般的だが、nullチェックを忘れるなど意図しないぬるぽが発生する危険性がある。
そこで、Optionを使用することで値が存在しないケースをnullを使わずに表現可能である。

// 値がある場合
val option: Option[String] = Option("hoge") // => Some("hoge")
// 値がない場合
val option: Option[String] = Option(null)  // => None

// パターンマッチによってOption値を取得する
val result: String = option match {
  case Some(x) => x
  case None => ""
}

例外は、Java同様throwキーワードでスローでき、try〜catch〜finallyで処理を行う。
なお、Scalaでは例外をtry〜catchしなくても良い(チェック例外と非チェック例外といった区別がない)。

try {
  ...
} catch {
  case e: IOException => { ... }
  case e => { ... }
} finally {
  ...
}

Scalaでの例外処理の手法として、メソッドの戻り値にscala.Eitherを使う方法がある。
Eitherは、型パラメータで指定した2つの型のうちどちらか一方の値を持つことができるコンテナである。
メソッドが正常に終了した場合にはメソッドの戻り値が、例外が発生した場合は例外がEitherに格納されるようにする。
その後、メソッドの呼び出し元でEitherの状態をチェックすれば、メソッドが正常終了したか否かを判定できる。

def readFile(name: String): Either[Throwable, String] = {
  val in = new FileInputStream(name)
  try {
    val buf = new Array[Byte](in.available())
    in.read(buf)
    // 処理が成功した場合、Right(=Eitherの右側=String)で結果を返す
    Right(new String(buf, "UTF-8"))
  } catch {
    // 例外が発生した場合、Left(=Throwable)で例外を返す
    case e => Left(e)
  } finally {
    in.close()
  }
}

上記のメソッドの呼び出し元では、以下のように結果を取りだすことができる。

val result Either[Throwable, String] = readFile("hoge.log")
result match {
  case Left(e) => e.printStackTrace()
  case Right(s) => println(s)
}

Scalaには呼び出し元での例外処理を強制するための仕組みがないため、呼び出し元でエラー処理が必要な場合には、
Eitherを返すことでエラーが発生したかどうかのチェックを強制するようにするのも1つのパターン。