LoginSignup
14
8

More than 5 years have passed since last update.

Scalaの基本を学ぶ

Posted at

はじめに

Scalaの基本を後から見直しやすいようにと思い要所要所まとめましたが、とても長い記事となっております。
逆引きのように見てもらえると幸いです。

動作環境

Scala:2.12

目次

Scalaとは

  • オブジェクト指向プログラミングと関数型プログラミングの統合
  • Javaと互換性があり、ライブラリも使用可能

Scalaの基本

  • 変数には、valvarの2種類がある
  • valは値の再代入が不可。varは値の再代入が可能。基本的にはvalのみでプログラミングする
    • Swiftでいうところの、let(再代入不可)とvar(再代入可能)

制御構文

if式

if (条件式) A {else B}

while式

while (条件式) A

for式(flatMapmapwithFilterforeachなどあるが、ここでは基本的な使い方)

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

継承

継承する目的

  1. スーパークラスの実装をサブクラスでも使うことで実装を再利用すること
  2. 複数のサブクラスが共通のスーパークラスのインターフェースを継承することで処理を共通化すること(サブタイピング・ポリモーフィズムと呼ぶ)
  • Scalaでは、トレイトという仕組みで複数の実装の継承を実現している。(下記のトレイトを参照)

クラスの継承)

class SubClass(....) extends SuperClass {
    ....
}

オブジェクト

  • Scalaでは、全ての値がオブジェクトで、全てのメソッドは何らかのオブジェクトに所属している

object構文の主な用途

  1. ユーティリティメソッドやグローバルな状態の置き場所
  2. オブジェクトのファクトリメソッド
  3. 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が優先されているため、ClassAgreetメソッドの呼び出しでTraitCgreetメソッドが実行されている。

自分型

  • 自分型を使う場合、抽象トレイトを指定し、後から実装を追加するという形になる
    • このように後(または外)から利用するモジュールの実装を与えることを「依存性の注入」と呼ぶ
  • 型の循環参照(相互参照)を許す

落とし穴:トレイトの初期化順序

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では共変

covariant.java
Object[] objects = new String[1];
objects[0] = 100;

このJavaのコードはコンパイルは通るが、
実行すると、java.lang.ArrayStoreExceptionが発生する。
これは、objectsに入っているのが、実際には、Stringの配列(Stringのみを要素として持つ)なのに、2行目でint型(ボクシング変換されてInteger型)の値である100を渡そうとしていることになる。

配列型はScalaでは非変なので、上記のコードをコンパイルするとその時点でエラーが表示される。

関数

Scalaの関数は、単にFunction0Function22までのトレントの無名サブクラスのインスタンスである。

2つの関数を取って加算した値を返すadd関数の定義例)

val add = new Function2[Int, Int, Int] {
    def apply(x: Int, y: Int): Int = x + y
}

無名関数

先ほどのように関数定義するとコードが冗長になり過ぎる。
そのため、Scalaでは、Function0Function22までのトレントのインスタンスを生成するためのシンタックスシュガーが用意されている。

シンタックスシュガーを使って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なコレクションを使うメリット

  1. 関数型プログラミングで多用する再起との相性が良い
  2. 高階関数を用いて簡潔なプログラムを書くことができる
  3. 一度作ったコレクションが知らない箇所で
  4. 並行に動作するプログラムの中で、安全に受け渡しすることができる

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を使うことはそれほど多くなく、ListVectorといったデータ構造を多用する。
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つの要素もマッチしなかった場合は、NoneOption型として返す。

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特有ではなく、
RangeArrayなどの他のコレクションでも同様に使うことができる。
その理由は、これらのメソッドによる操作の大半は、コレクションのスーパークラスである共通トレイト中に宣言されているからである。
各メソッドの詳細は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との列挙型との違いは、

  • 各々のデータは独立してパラメータを持つことができること

パターンマッチングを用いて定義することで、

  1. ノードの種類と構造によって分岐する
  2. ネストしたノードを分解する
  3. ネストしたノードを分解した結果を変数に束縛する

という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でのエラー処理は

  1. 例外を使う方法
  2. 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

Optionnullが入っている場合の実行例)

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

mapflattenは、実際はこの両方を組み合わせて使うことが多い。
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はSomeNoneの2つの値を持つが、EitherはRightLeftの2つの値を持つ。

Try

TryはEitherと同じように正常な値とエラー値のどちらかを表現するデータ型。
Eitherとの違いは、2つの型が平等ではなく、エラー値がThrowableに限定されており、型引数を1つしか取らないこと。
具体的には、TryはSuccessFailureの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は大きく分けて二通りの使われ方をする

  1. 新しく定義したユーザー定義の型などを既存の型に当てはめたい場合
  2. 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は大きく分けて二通りの使われ方をする

  1. メソッドで共通に引き渡されるオブジェクト(ソケットやDBのコネクションなど)を明示的に引き渡すのを省略するために使う
  2. 型クラスを定義・使用することを可能にするために使う

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を定義する。
ここでは、StringIntについて定義する。

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型ListString型のListのどちらの要素の合計も計算できる汎用的なsumメソッドができた。
まとめると、以下のようになる。

Int型ListString型の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に基づき、内容をまとめさせていただきました。

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8