はじめに
Scalaの基本を後から見直しやすいようにと思い要所要所まとめましたが、とても長い記事となっております。
逆引きのように見てもらえると幸いです。
動作環境
Scala:2.12
目次
- Scalaとは
- Scalaの基本
- 制御構文
- クラス
- オブジェクト
- トレイト
- 型パラメータと変位指定
- 関数
- コレクションライブラリ(immutable と mutable)
- ケースクラスとパターンマッチング
- エラー処理
- Implicit
Scalaとは
- オブジェクト指向プログラミングと関数型プログラミングの統合
- Javaと互換性があり、ライブラリも使用可能
Scalaの基本
- 変数には、
val
とvar
の2種類がある -
val
は値の再代入が不可。var
は値の再代入が可能。基本的にはval
のみでプログラミングする- Swiftでいうところの、
let
(再代入不可)とvar
(再代入可能)
- Swiftでいうところの、
制御構文
if式
if (条件式) A {else B}
while式
while (条件式) A
for式(flatMap
・map
・withFilter
・foreach
などあるが、ここでは基本的な使い方)
for(ジェネレータ1; ジェネレータ2; ... ジェネレータn) A
# ジェネレータ1 = a1 <- exp1; ジェネレータ2 = a2 <- exp2; ... ジェネレータn = an <- expn
for式の例)
for (x <- 1 to 5; y <- 1 until 5) {
println("x = " + x + "y =" + y)
}
-
1 to 5
は1から5まで(5を含む)の範囲 -
1 until 5
は1から5まで(5を含まない)の範囲
yield
を使ったfor式の例)
for (e <- List("A", "B", "C", "D", "E")) yield {
"Pre" + e
}
-
for
構文はyield
キーワードを使うことで、コレクションの要素を加工して返すという全く異なる要素に使うことが可能
match式
マッチ対象の式 match {
case パターン1 [if ガード1] => 式1
case パターン2 [if ガード2] => 式2
case パターン3 [if ガード3] => 式3
case ...
case パターンN [if ガードN] => 式N
}
- 漏れがないようにするために、パターンにワイルドカード(
_
など)を使用することも可能
クラス
フィールド定義
(private[this/package名]/protected[package名]) (val/var) fieldName: Type = Expression
※ private[this]
を付けたフィールドへのアクセスは一般にJVMレベルでのフィールドへの直接アクセスになるため、若干高速
抽象メンバー
その時点では実装を書くことができず、後述する継承の際に、メソッド・フィールドの実装を与えたいという時に、抽象メンバーを定義する。
抽象メンバーのメソッド実装例)
(private[this/package名]/protected[package名]) def methodName(parameter1: Type1, parameter2: Type2, ...): ReturnType
抽象メンバーのフィールド実装例)
(private[this/package名]/protected[package名]) (val/var) fieldName: Type
継承
継承する目的
- スーパークラスの実装をサブクラスでも使うことで実装を再利用すること
- 複数のサブクラスが共通のスーパークラスのインターフェースを継承することで処理を共通化すること(サブタイピング・ポリモーフィズムと呼ぶ)
- Scalaでは、トレイトという仕組みで複数の実装の継承を実現している。(下記のトレイトを参照)
クラスの継承)
class SubClass(....) extends SuperClass {
....
}
オブジェクト
- Scalaでは、全ての値がオブジェクトで、全てのメソッドは何らかのオブジェクトに所属している
object
構文の主な用途
- ユーティリティメソッドやグローバルな状態の置き場所
- オブジェクトのファクトリメソッド
- Singletonパターン
object
の基本構文)
object オブジェクト名 extends クラス名 with トレイト名2 ... {
本体
}
- クラスと同じファイル内、同じ名前で定義されたシングルトンオブジェクトは、コンパニオンオブジェクトと呼ばれる
- コンパニオンオブジェクトは、対応するクラスに対して特権的なアクセス権を持つ
トレイト
- Scalaのトレイトはクラスからコンストラクタを定義する機能を抜いたようなもの
トレイトの定義)
trait TraitName {
(a filed or a method definition)の0回以上の繰り返し
}
トレイトの基本
クラスに比べて以下のような特徴がある
- 複数のトレイトを1つのクラスやトレイトにミックスインできる
- 複数のクラスを継承させたい場合は、クラスをトレイトにする必要がある
- 直接インスタンス化できない
- トレントを使うときは通常、それを継承したクラスを作る。これは、トレントが単体で使われることを想定していないための制限である。
- クラスパラメータ(コンストラクタの引数)を取ることができない
- トレイトに抽象メンバを持たせることで値を渡すことが可能
trait TraitA {
val name: String
def printName(): Unit = println(name)
}
// クラスにしてnameを上書きする
class ClassA(val name: String) extends TraitA
object ObjectA {
val a = new ClassA("Objective-C")
// nameを上書きするような実装を与えてもよい
val a2 = new TraitA { val name = "Swift" }
}
菱形継承問題を解決するための線形化(linearization)
継承関係が複雑になったメソッドを全て明示的に呼ぶのは大変であるため、
トレイトがミックスインされた順番をトレイトの継承順番とみなす線形化という機能がある。
trait TraitA {
def greet(): Unit
}
trait TraitB extends TraitA {
override def greet(): Unit = println("Good morning!")
}
trait TraitC extends TraintA {
override def greet(): Unit = println("Good evening!")
}
class ClassA extends TraitB with TraitC
実行文)
scala> (new ClassA).greet()
Good everning!
トレイトの継承順番が線形化され、後からミックスインしたTraitC
が優先されているため、ClassA
のgreet
メソッドの呼び出しでTraitC
のgreet
メソッドが実行されている。
自分型
- 自分型を使う場合、抽象トレイトを指定し、後から実装を追加するという形になる
- このように後(または外)から利用するモジュールの実装を与えることを「依存性の注入」と呼ぶ
- 型の循環参照(相互参照)を許す
落とし穴:トレイトの初期化順序
trait A {
val foo: String
}
trait B extends A {
val bar = foo + "World"
}
class C extends B {
val foo = "Hello"
def printBar(): Unit = println(bar)
}
実行文)
scala> (new C).printBar()
nullWorld
Scalaのクラス・トレントはスーバークラスから順番に初期化される。
そのため、この例では、クラスC
はトレイトB
を継承し、トレイトB
はトレイトA
を継承している。
つまり、初期化はトレイトA
が一番先に行われ変数bar
が宣言され、中身は何も代入していないので、nullになる。次に、トレントB
で変数bar
が宣言され、null
であるfoo
と"World"という文字列から"nullWorld"
という文字列が作られ、変数bar
に代入される。
トレイトのval
の初期化順序の回避方法
trait A {
val foo: String
}
trait B extends A {
lazy val bar = foo + "World" //もしくは def bar でもよい
}
class C extends B {
val foo = "Hello"
def printBar(): Unit = println(bar)
}
実行文
cala
scala> (new C).printBar()
HelloWorld
bar
の初期化にlazy val
を使うことで、bar
の初期化が実際に行われるまで遅延されることがなくなる。
型パラメータと変位指定
Scalaでは、何も指定しなかった型パラメータは通常は非変(invariant)になる。
共変(covariant)
配列型はJavaでは共変
Object[] objects = new String[1];
objects[0] = 100;
このJavaのコードはコンパイルは通るが、
実行すると、java.lang.ArrayStoreException
が発生する。
これは、objects
に入っているのが、実際には、String
の配列(String
のみを要素として持つ)なのに、2行目でint
型(ボクシング変換されてInteger型
)の値である100
を渡そうとしていることになる。
配列型はScalaでは非変なので、上記のコードをコンパイルするとその時点でエラーが表示される。
関数
Scalaの関数は、単にFunction0
〜 Function22
までのトレントの無名サブクラスのインスタンスである。
2つの関数を取って加算した値を返すadd
関数の定義例)
val add = new Function2[Int, Int, Int] {
def apply(x: Int, y: Int): Int = x + y
}
無名関数
先ほどのように関数定義するとコードが冗長になり過ぎる。
そのため、Scalaでは、Function0
〜 Function22
までのトレントのインスタンスを生成するためのシンタックスシュガーが用意されている。
シンタックスシュガーを使ってadd
関数を定義した例)
val add = (x: Int, y: Int) => x + y
実行例)
scala> add(100, 200)
res1: Int = 300
関数のカリー化
カリー化とは、(Int, Int) => Int
型の関数のように複数の引数を取る関数があったとき、
これをInt => Int => Int
型の関数のように1つの引数を取り、残りの引数を取る関数を返す関数のチェインで表現するというもの。
上記のadd
をカリー化したaddCurried
関数を定義した例
val addCurried = (x: Int) => ((y: Int) => x + y)
実行例)
scala> addCurried(100)(200)
res1: Int = 300
コレクションライブラリ(immutable と mutable)
Scalaには、一度作成したら変更できない(immutable)なコレクションと変更できる(mutable)通常のコレクションがある。
immutableなコレクションを使うメリット
- 関数型プログラミングで多用する再起との相性が良い
- 高階関数を用いて簡潔なプログラムを書くことができる
- 一度作ったコレクションが知らない箇所で
- 並行に動作するプログラムの中で、安全に受け渡しすることができる
Array(mutable)
Arrayの定義例)
val arr = Array(1, 2, 3, 4, 5)
// 型を省略せずに書くと
val arr = Array[Int](1, 2, 3, 4, 5)
ここで、[Int]
の部分は型パラメータと呼ぶ。
Array
だけではどの型か分からないので、[Int]
と付けることでどの型のArray
かを指定している。(しかし、この場面では、Array
の要素型はInt
だとわかっているので、冗長な書き方)
この型パラメータは型推論を補うために、色々な箇所で出てくる。
Range
Range
は範囲を表すオブジェクト
Range
の使用例)
scala> 1 to 5
res1: scala.collection.immutable.Range.Inclusive = Range 1 to 5
scala> (1 to 5).toList
res2: List[Int] = List(1, 2, 3, 4, 5)
scala> 1 until 5
res3: scala.collection.immutable.Range = Range 1 until 5
scala> (1 until 5).toList
res4: List[Int] = List(1, 2, 3, 4)
List(immutable)
ScalaではArray
を使うことはそれほど多くなく、List
やVector
といったデータ構造を多用する。
List
の特徴は、一度作成したら中身を変更できない(immutable)であること。
中身を変更できないデータ構造(永続データ構造と呼ぶ)はScalaがサポートしている関数型プログラミングにとって重要な要素。
List
の実行例)
scala> val lst = List(1, 2, 3, 4, 5)
lst: List[Int] = List(1, 2, 3, 4, 5)
scala> lst(0) = 7
<console>:13: error: value update is not a member of List[Int]
lst(0) = 7
^
List
は値を更新することはできないが、その代わりとしてList
を元に新たなList
を作ることができる。
Listの先頭に要素をくっつける
::
(コンスと読む)は既にあるList
の先頭に要素をくっつけるメソッド
実行例)
scala> val a1 = 1 :: Nil
a1: List[Int] = List(1)
scala> val a2 = 2 :: a1
a2: List[Int] = List(2, 1)
List同士の連結
++
はList同士を連結するメソッド
実行例)
scala> List(1, 2) ++ List(3, 4)
res1: List[Int] = List(1, 2, 3, 4)
※ 大きなList
同士を連結する場合、計算量が大きくなるので注意
mkString:文字列のフォーマッティング
mkStringフォーマッティングのやり方は3種類ある。
- mkString(引数なしバージョン)
scala> List(1, 2, 3, 4, 5).mkString
res1: String = 12345
※ 引数なしメソッドのmkString
は()
を付けて読みだすことができない。
逆に()
を使って定義されたメソッドは、()
を付けても付けなくても良いことになっているので注意
- mkString(sep: String)
scala> List(1, 2, 3, 4, 5).mkString(",")
res1: String = 1,2,3,4,5
- mkString(start: String, sep: String, end: String)
scala> List(1, 2, 3, 4, 5).mkString("[", ",", "]")
res1: String = [1,2,3,4,5]
foldLeft:左からの畳み込み
foldLeftメソッドの宣言)
def foldLeft[B](z: B)(f: (B, A) => B): B
実行例)
// Listの要素の合計を求める場合の例
scala> List(1, 2, 3).foldLeft(0)((x, y) => x + y)
res0: Int = 6
// Listの要素を全て掛け合わせた結果を求める場合の例
scala> List(2, 2, 3).foldLeft(1)((x, y) => x * y)
res1: Int = 12
与える引数の順序がちょうど対称となっているfoldRight
メソッドも存在する。
map:各要素を加工した新しいList
を返す
map
メソッドは、1引数の関数を引数に取り、各要素に関数を適用した結果で生まれた要素からなる新たなList
を返す
Listの各要素を2倍したmap
メソッドの実行例)
scala> List(1, 2, 3, 4, 5).map(x => x * 2)
res0: List[Int] = List(2, 4, 6, 8, 10)
x => x * 2
の部分は、無名関数を定義するための構文。
filter:条件に合った要素だけを抽出した新しいList
を返す
filter
メソッドはBoolean
型を返す1引数の関数を引数に取り、各要素に関数を適用し、true
になった要素のみを抽出した新たなList
を返す
List内の奇数だけ抽出する実行例)
scala> List(1, 2, 3, 4, 5).filter(x => x % 2 == 1)
res0: List[Int] = List(1, 3, 5)
find:条件に合った最初の要素を返す
find
メソッドはBoolean
型を返す1引数の関数を引数に取り、各要素に前から順番に関数を適用し、最初にtrue
になった要素をSome
でくるみ、Option
型をして返す。1つの要素もマッチしなかった場合は、None
をOption
型として返す。
List内の最初の奇数だけを抽出する実行例)
scala> List(1, 2, 3, 4, 5).find(x => x % 2 == 1)
res0: Option[Int] = Some(1)
takeWhile:先頭から条件を満たしている間を抽出する
takeWhile
メソッドは、Boolean
型を返す1引数の関数を引数に取り、前から順番に関数を適用し、結果がtrue
の間のみからなるList
を返す
List内の5より前の4要素を抽出する実行例)
scala> List(1 ,2 ,3, 4, 5).takeWhile(x => x != 5)
res0: List[Int] = List(1, 2, 3, 4)
count:List
の中で条件を満たしている要素の数を計算する
count
メソッドは、Boolean
型を返す1引数の関数を引数に取り、全ての要素に関数を適用し、true
が返ってきた要素の数を計算する。
List内の偶数の数を計算する例)
scala> List(1, 2, 3, 4, 5).count(x => x % 2 == 0)
res0: Int = 2
flatMap:List
を平らにする
flatMap
メソッドの宣言)
final def flatMap[B](f: (A) => GenTraversableOnce[B]): List[B]
GenTraversableOnce[B]
はあらゆるコレクション(要素の型はB型である)を入れることができる型と考える。
flatMap
の使用例1)
scala> List(List(1, 2, 3), List(4, 5)).flatMap{e => e.map{g => g + 1}}
res0: List[Int] = List(2, 3, 4, 5, 6)
使用例1は、ネストしたList
の各要素にflatMap
の中でmap
を適用し、List
の各要素に1を足したものを平らにしている。
flatMap
の使用例2)
scala> List(1, 2, 3).flatMap{e => List(4, 5).map(g => e * g)}
res0: List[Int] = List(4, 5, 8, 10, 12, 15)
使用例2は、List(1, 2, 3)
とList(4, 5)
の2つのList
についてループし、各々の要素を掛け合わせた要素からなるList
を抽出している
Listの性能特性
-
List
の先頭要素へのアクセスは高速にできる - 要素へのランダムアクセスや末尾へのデータ追加は、
List
の長さに比例した時間がかかってしまう
紹介したメソッドについて
mkString
をはじめとしたflatMap
などList
の色々なメソッドを紹介してきたが、これらの大半は、List
特有ではなく、
Range
やArray
などの他のコレクションでも同様に使うことができる。
その理由は、これらのメソッドによる操作の大半は、コレクションのスーパークラスである共通トレイト中に宣言されているからである。
各メソッドの詳細はScalaのAPIドキュメントより。
他のコレクションライブラリについては、以下の公式ドキュメントを参照
Vector(immutable)
Vector
は一度データ構造を構築したら変更できないimmutableなデータ構造である。
要素へのランダムアクセス・長さの取得・データの挿入や削除などいずれの操作も十分高速にできる。
Vector
の使用例)
scala> Vector(1, 2, 3, 4, 5)
res0: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5)
scala> Vector(1, 2, 3, 4, 5) :+ 6
res1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5, 6)
scala> Vector(1, 2, 3, 4, 5).updated(2, 8)
res2: scala.collection.immutable.Vector[Int] = Vector(1, 2, 8, 4, 5)
Map(immutable・mutable)
Map
はキーから値へのマッピングを提供するデータ構造。(他の言語では、辞書や連想配列と呼ばれたりする)
Scalaでは、一度作成したら変更できないimmutableなMap
と変更できるmutableなMap
の2種類が提供されている。
Scalaでは、何も設定せずMap
と書いた場合、scala.collection.immutable.map
が使われる。
scala.collection.immutable.map
の使用例)
scala> val m = Map("A" -> 1, "B" -> 2, "C" -> 3)
m: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2, C -> 3)
scala> m.updated("A", 0) //一見元のMapが変更されたように見えるが
res1: scala.collection.immutable.Map[String,Int] = Map(A -> 0, B -> 2, C -> 3)
scala> m // 元のMapはそのままで変更されていない
res2: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2, C -> 3)
scala.collection.mutable.map
の使用例)
scala> import scala.collection.mutable
import scala.collection.mutable
scala> val m = mutable.Map("A" -> 1, "B" -> 2, "C" -> 3)
m: scala.collection.mutable.Map[String,Int] = Map(A -> 1, C -> 3, B -> 2)
scala> m("A") = 0 // A -> 0のマッピングに置き換える
scala> m // 変更が反映されている
res0: scala.collection.mutable.Map[String,Int] = Map(A -> 0, C -> 3, B -> 2)
Set(immutable・mutable)
Set
は値の集合を提供するデータ構造。
Set
の中では同じ値が2以上存在しない。(重複した値は自動で削除される)
Map
と同じでScalaでは何も設定せずSet
と書くと、scala.colletion.immutable.Set
が使われる
scala.colletion.immutable.Set
の使用例)
scala> val s = Set(1, 1, 3, 4, 5)
s: scala.collection.immutable.Set[Int] = Set(1, 3, 4, 5)
scala> s - 5 // 5を削除した後も
res0: scala.collection.immutable.Set[Int] = Set(1, 3, 4)
scala> s // 元のSetはそのまま
res1: scala.collection.immutable.Set[Int] = Set(1, 3, 4, 5)
scala.colletion.mutable.Set
の使用例)
scala> import scala.collection.mutable
import scala.collection.mutable
scala> val s = mutable.Set(1, 1, 3, 4, 5)
s: scala.collection.mutable.Set[Int] = Set(1, 5, 3, 4)
scala> s -= 5 // 5を削除したら
res0: s.type = Set(1, 3, 4)
scala> s // 変更が反映される
res1: scala.collection.mutable.Set[Int] = Set(1, 3, 4)
ケースクラスとパターンマッチング
簡単なケースクラスによるデータ型の定義
sealed abstract class DayOfWeek
case object Sunday extends DayOfWeek
case object Monday extends DayOfWeek
case object Tuesday extends DayOfWeek
case object Wednesday extends DayOfWeek
case object Thursday extends DayOfWeek
case object Friday extends DayOfWeek
case object Saturday extends DayOfWeek
CやJavaとの列挙型との違いは、
- 各々のデータは独立してパラメータを持つことができること
パターンマッチングを用いて定義することで、
- ノードの種類と構造によって分岐する
- ネストしたノードを分解する
- ネストしたノードを分解した結果を変数に束縛する
という3つの動作が同時に行える。
ある日の次の曜日を返すメソッドnextDayOfWeek
をパターンマッチを用いて定義している例)
def nextDayOfWeek(d: DayOfWeek): DayOfWeek = d match {
case Sunday => Monday
case Monday => Tuesday
case Tuesday => Wednesday
case Wednesday => Thursday
case Thursday => Friday
case Friday => Saturday
case Saturday => Sunday
}
実行例)
scala> nextDayOfWeek(Sunday)
res0: DayOfWeek = Monday
※ 変数宣言でもパターンマッチングを行うことが可能
変数宣言でパターンマッチングを行なっている例)
scala> case class Point(x: Int, y: Int)
defined class Point
scala> val Point(x, y) = Point(10, 20)
x: Int = 10
y: Int = 20
部分関数
無名関数とパターンマッチングの2つの機能を組み合わせた、部分関数(PartialFunction)がScalaには存在する
無名関数の具体的なユースケース)
scala> List(1, 2, 3, 4 ,5).collect { case i if i % 2 == 1 => i * 2 }
res0: List[Int] = List(2, 6, 10)
ここで、collect
メソッドは、pf: PartialFunction[A, B]
を引数に取り、pf.isDefinedAt(i)
がtrue
になる要素のみを残し、さらに、pf.apply(i)
の結果の値を元にした新しいコレクションを返します。
isDefinedAt
は、
i % 2 == 1
の部分から自動的に生成され、パターンがマッチするときのみ真(true
)になるように定義される。
エラー処理
Scalaでのエラー処理は
- 例外を使う方法
- OptionやEitherやTryなどのデータ型を使う方法
の2つの方法があり、状況に応じて使い分けることになる。
エラー処理で実現しなければならないこと
- 例外安全性
- 例外が発生してもシステムがダウンしたり、データの不整合などの問題が起きないようにする概念のこと
- 強い例外安全性
- 例外が発生した場合、全ての状態が例外発生前に戻らなければならないという概念のこと
例外の問題点
Javaのエラー処理は例外が中心的な役割を担っていて、Scalaでも例外は多く使われる。しかし、例外は便利な反面、以下のような問題もある。
- 例外と使うとコントロールフローがわかりづらくなる
- 適切に使えば正常系の処理とエラー処理を分離し、コードの可読性を上げ、エラー処理をまとめる効果があるが、例外のcatch漏れや、例外をcatchしているところで、どこで発生した例外をcatchしているのか判別できないため、コードの修正を阻害する場合もある
- 例外は非同期プログラミングでは使えない
- 例外の動作は送出されたらcatchされるまで、コールスタックを遡っていくというもの。そのため、別スレッド・別のイベントループなどで実行される非同期プログラミングでは相容れない
- 例外は型チェックできない
- チェック例外を使わない場合、メソッドの型として表現されない。(Scalaでは、高階関数でチェック例外を扱うことが難しいという問題が大きく、チェック例外の機能は無くなっている)また、間違った例外をキャッチしているかは、実行時にしか分からない。
Option
OptionはScalaで多用されるデータ型の1つ。
Option型には、以下の2つの値が存在する。
Some
None
Option
に具体的な値が入っている場合の実行例)
scala> val o: Option[String] = Option("hoge")
o: Option[String] = Some(hoge)
scala> o.get
res0: String = hoge
scala> o.isEmpty
res1: Boolean = false
scala> o.isDefined
res2: Boolean = true
Option
にnull
が入っている場合の実行例)
scala> val o: Option[String] = Option(null)
o: Option[String] = None
scala> o.isEmpty
res0: Boolean = true
scala> o.isDefined
res1: Boolean = false
Optionのパターンマッチ
Optionは型を持っているため、パターンマッチを使って処理することもできる。
実行例)
scala> val s: Option[String] = Some("hoge")
s: Option[String] = Some(hoge)
scala> val result = s match {
| case Some(str) => str
| case None => "not matched"
| }
result: String = hoge
上記のようにSomeかNoneにパターンマッチすることで、Someにパターンマッチする場合には、その中身の値をstr
という別の変数に束縛することが可能。
Optionの入れ子を解消する
キャッシュから情報を取得する場合、キャッシュヒットする場合と、キャッシュミスする場合があり、それらは、ScalaではOption型で表現されることが多い。
以下のようなキャッシュ取得が連続で繰り返された場合、Optionが入れ子になってしまう。
1つ目と2つ目の整数の値がOptionで返ってきて、それをかけた値を求める実行例)
scala> val v1: Option[Int] = Some(3)
v1: Option[Int] = Some(3)
scala> val v2: Option[Int] = Some(5)
v2: Option[Int] = Some(5)
scala> v1.map(i1 => v2.map(i2 => i1 * i2))
res0: Option[Option[Int]] = Some(Some(15))
Optionの入れ子を解消するためには、flatten
を実行すれば良い。
flatten
を実行し、Optionの入れ子を解消している例)
scala> val v1: Option[Int] = Some(3)
v1: Option[Int] = Some(3)
scala> val v2: Option[Int] = None
v2: Option[Int] = None
scala> v1.map(i1 => v2.map(i2 => i1 * i2)).flatten
res0: Option[Int] = None
上記のように、v2がNoneである場合でも、flatten
は成立する。そのため、キャッシュミスでSomeの値が取れなかった際も問題なく処理が動く。
flatMap
map
とflatten
は、実際はこの両方を組み合わせて使うことが多い。
flatMap
は、Optionにmap
をかけてflatten
を適用してくれるメソッド
scala> val v1: Option[Int] = Some(3)
v1: Option[Int] = Some(3)
scala> val v2: Option[Int] = Some(5)
v2: Option[Int] = Some(5)
scala> val v3: Option[Int] = Some(7)
v3: Option[Int] = Some(7)
scala> v1.flatMap(i1 => v2.flatMap(i2 => v3.map(i3 => i1 * i2 * i3)))
res0: Option[Int] = Some(105)
Either
Eitherは、エラー時にエラーの種類まで取得できる。
Optionでは、Noneが返ってきた場合、値が取得できなかったことはわかるがエラーの状態は取得できないので、エラーの種類がわからなくても問題にならない場合のみ使える。
具体的には、OptionはSome
とNone
の2つの値を持つが、EitherはRight
とLeft
の2つの値を持つ。
Try
TryはEitherと同じように正常な値とエラー値のどちらかを表現するデータ型。
Eitherとの違いは、2つの型が平等ではなく、エラー値がThrowableに限定されており、型引数を1つしか取らないこと。
具体的には、TryはSuccess
とFailure
の2つの値を持つ。
Success
は型変数を取り任意の値を入れることができるが、Failure
はThrowableしか入れることができない。
そして、Tryには、コンパニオンオブジェクトのapplyで生成する際に、例外をcatchしFailureにする機能がある。
Tryの実行例)
scala> import scala.util.Try
import scala.util.Try
scala> val v: Try[Int] = Try(throw new RuntimeException("to be caught"))
v: scala.util.Try[Int] = Failure(java.lang.RuntimeException: to be caught)
この機能を使って、例外が起こりそうな箇所をTry
で包み、Failure
にして値として扱えるようにするのがTryの特徴
scala> val v1 = Try(3)
v1: scala.util.Try[Int] = Success(3)
scala> val v2 = Try(5)
v2: scala.util.Try[Int] = Success(5)
scala> val v3 = Try(7)
v3: scala.util.Try[Int] = Success(7)
scala> for {
| i1 <- v1
| i2 <- v2
| i3 <- v3
| } yield i1 * i2 * i3
res0: scala.util.Try[Int] = Success(105)
NonFatal
の例外
Try.apply
がcatchするのは全ての例外ではなく、NonFatalという種類の例外のみ。
その理由は、NonFatalでないエラーはアプリケーション中で復旧が困難で重度なものであるから。
Try以外でも、扱うことができる全ての例外をまとめて処理する場合などに以下のようなコードが出てくることがある。
import scala.util.control.NonFatal
try {
???
} catch {
case NonFatal(e) => // 例外の処理
}
OptionとEitherとTryの使い分け
- Optionは、Javaでnullを使うような場面で使うのが良い。
- Eitherは、Optionを使うのでは情報が不足しており、エラー状態が代数的データ型として定められるものに使うのが良い。Javaでチェック例外を使っていたような復帰可能なエラーだけに使うという考え方でも良い。
- Tryは、Javaの例外をどうしても値として扱いたい場合に使うのが良い。非同期プログラミングや、実行結果を保存し中身を参照する場合などに使うことも考えられる。
Implicit
Implicit Conversion(暗黙の型変換)
implicit conversionは暗黙の型変換機能をユーザーが定義できるようにする機能。
implicit conversionの定義例)
implicit def メソッド名(引数名: 引数の型): 返り値の型 = 本体
implicit conversionは大きく分けて二通りの使われ方をする
- 新しく定義したユーザー定義の型などを既存の型に当てはめたい場合
- pimp my libraryパターンと呼ばれる、既存のクラスにメソッドを追加し拡張する(ようにみせかける)使い方
1つ目の実行例)
scala> implicit def intToBoolean(arg: Int): Boolean = arg != 0
intToBoolean: (arg: Int)Boolean
scala> if(1) {
| println("1はTrueである")
| }
1はTrueである
といった形で、本来Boolean
しか渡せないはずにif式にInt
を渡すことができる。
※ implicit conversion を定義することで、コンパイラで本来はif文の条件式にはBoolean
型の式しか渡せないようチェックしているものを通り抜けてしまうため、本当にその変換が必要であるかよく考える必要がある
2つ目の実行例1)
scala> class RichString(val src: String) {
| def smile: String = src + "(^ ^)"
| }
defined class RichString
scala> implicit def enrichString(arg: String): RichString = new RichString(arg)
enrichString: (arg: String)RichString
scala> "Hi! ".smile
res0: String = Hi! (^ ^)
コンパイラは、ある型に対するメソッド呼び出しを見つけたとき、そのメソッドを定義した型がimplicit conversionの返り値の型にないか検索し、型が合ったらimplicit conversionの呼び出しを挿入する。
2つ目の実行例1 では、String
の末尾に"(^ ^)"
という文字列を追加して返すimplicit conversionを定義している。
2つ目の実行例1は、Scala 2.10以降では、以下のように書き直すことが可能
2つ目の実行例2)
scala> implicit class RichString(val src: String) {
| def smile: String = src + "(^ ^)"
| }
defined class RichString
scala> "Hi! ".smile
res0: String = Hi! (^ ^)
Implicit Parameter(暗黙のパラメータ)
implicit Parameterは大きく分けて二通りの使われ方をする
- メソッドで共通に引き渡されるオブジェクト(ソケットやDBのコネクションなど)を明示的に引き渡すのを省略するために使う
- 型クラスを定義・使用することを可能にするために使う
1つ目の使い方の具体例)
以下のようなDBとのコネクションを表すConnection
型があるとする。DBと接続するメソッドは、全てこのConnection
型を引き渡さなければならない。
def useDatabase1(..../ conn: Connection)
def useDatabase2(..../ conn: Connection)
def useDatabase3(..../ conn: Connection)
この3つのメソッドは共通してConnection
型を引数に取るにも関わらず、呼び出す度に明示的にConnection
オブジェクトを渡さなければならず面倒である。
上のメソッド定義をimplicit parameterを使い以下のように書き換える。
def useDatabsse1(....)(implicit conn: Connection)
def useDatabsse2(....)(implicit conn: Connection)
def useDatabsse3(....)(implicit conn: Connection)
Scalaのコンパイラは、このように定義されたメソッドが呼び出されると、現在のスコープからたどって直近のimplicitとマークされた値を暗黙でメソッドに引き渡す。
値をimplicitとしてマークする例)
implicit val connection: Connection = connectDatabase(....)
このようなimplicit parameterの使い方は、Play 2 FrameworkやScalaの各種O/Rマッパーで頻出する。
2つめの使い方の具体例として、List
の全ての要素の値を加算した結果を返すsum
メソッドを定義したいとする。
この際、「何の」List
か全く分かっていない場合、整数の+
メソッドをそのまま使うことはできない。
この問題を解決するために、2つの手順を踏む。
まず、2つの同じ型を足す(0の場合はそれに相当する値を返す)方法を知っている型を定義する。
ここでは、その型をAddtive
とする。
Additive
の定義)
trait Additive[A] {
def plus(a: A, b: A): A
def zero: A
}
ここで、Additive
の型パラメータA
は加算されるList
の要素の型を表している。また、
-
zero
:型パラメータAの0に相当する値を返す -
plus
:型パラメータAを持つ2つの値を加算して返す
となっている。
次に、このAdditive
型を使って、List
の全ての要素を合計するメソッドを定義する
def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
後は、それぞれの型に応じた加算と0の定義を持ったobjectを定義する。
ここでは、String
とInt
について定義する。
object StringAdditive extends Additive[String] {
def plus(a: String, b: String): String = a + b
def zero: String = ""
}
object IntAdditive extends Additive[Int] {
def plus(a: Int, b: Int): Int = a + b
def zero: Int = 0
}
これで、Int型
のList
もString
型のList
のどちらの要素の合計も計算できる汎用的なsum
メソッドができた。
まとめると、以下のようになる。
Int型
のList
もString
型のList
のどちらの要素の合計も計算できるsum
メソッドの実装例)
trait Additive[A] {
def plus(a: A, b: A): A
def zero: A
}
object StringAdditive extends Additive[String] {
def plus(a: String, b: String): String = a + b
def zero: String = ""
}
object IntAdditive extends Additive[Int] {
def plus(a: Int, b: Int): Int = a + b
def zero: Int = 0
}
def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
sum
メソッドの実行例)
scala> sum(List(1, 2, 3))(IntAdditive)
res0: Int = 6
scala> sum(List("S", "w", "i", "f", "t"))(StringAdditive)
res1: String = Swift
これで、目的は果たすことができたが、sum
メソッドを使えば何のList
の要素を合計するかは型チェックで分かっているのだから、引数にIntAdditive
,StringAdditive
を明示的に渡すことなく、推論してほしい。
前置きが長くなってしまったが、これをimplicit parameterで実現することができる。
方法は、
-
IntAdditive
,StringAdditive
の定義の前にimplicitと付ける -
sum
の最後の引数リストのm
にimplicitを付ける
だけである。
implicit parameterを使い、IntAdditive
,StringAdditive
を明示的に渡すことなく推論可能にした形は以下の通りである。
trait Additive[A] {
def plus(a: A, b: A): A
def zero: A
}
implicit object StringAdditive extends Additive[String] {
def plus(a: String, b: String): String = a + b
def zero: String = ""
}
implicit object IntAdditive extends Additive[Int] {
def plus(a: Int, b: Int): Int = a + b
def zero: Int = 0
}
def sum[A](lst: List[A])(implicit m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
実行例)
scala> sum(List(1, 2, 3))
res0: Int = 6
scala> sum(List("S", "w", "i", "f", "t"))
res1: String = Swift
この用法は、Scalaの標準ライブラリにも使われており、
scala> List(1, 2, 3, 4).sum
res0: Int = 10
scala> List(1.5, 2.5, 3, 4).sum
res1: Double = 11.0
のように、整数や浮動小数点数の合計値を気にすることなく計算することができる。
このように、型クラスを定義・使用することを可能にするために使うことで、コード設計の幅が広がる。
おわりに
内容を振り返ってみて、Scalaの基礎文法などを一通り学ぶことができました。これからも、後から見直しやすいようにまとめていくことは続けていきたいと思います。
本記事は、
Scala研修テキストよりライセンスCC BY-NC-SA 3.0に基づき、内容をまとめさせていただきました。