変数と定数
値や参照を保持するための基本的な宣言方法です。
- 定数 (Immutable): 再代入不可。
val
を使用します。 - 変数 (Mutable): 再代入可能。
var
を使用します。 - 型推論: Scalaコンパイラが型を自動的に推測します。
- 型指定: 明示的に型を指定することも可能です。
// 定数 (再代入不可)
val message: String = "こんにちは、Scala!" // 型を明示的に指定
val count = 100 // 型推論 (Int)
// 変数 (再代入可能)
var mutableValue = 10.5 // 型推論 (Double)
mutableValue = 20.0 // 再代入OK
// 型を指定した変数
var index: Int = 0
index = 1
// valは再代入できない
// count = 200 // これはコンパイルエラーになります 🚫
基本的なデータ型
Scalaでよく使われる基本的なデータ型の一覧です。Scalaでは、これらのプリミティブ型に対応するクラスが用意されており、すべてオブジェクトとして扱われます。
型 | 説明 | 例 |
---|---|---|
Byte |
8ビット符号付き整数 | val b: Byte = 127 |
Short |
16ビット符号付き整数 | val s: Short = 32767 |
Int |
32ビット符号付き整数 | val i: Int = 2147483647 |
Long |
64ビット符号付き整数 | val l: Long = 9223372036854775807L |
Float |
32ビット単精度浮動小数点数 | val f: Float = 3.14f |
Double |
64ビット倍精度浮動小数点数 | val d: Double = 3.1415926535 |
Char |
16ビットUnicode文字 | val c: Char = 'A' |
String |
文字列 (JavaのString) | val str: String = "Scala文字列" |
Boolean |
真偽値 (true または false) | val flag: Boolean = true |
Unit |
値を返さないことを示す型 (Javaのvoidに相当) | def printMessage(): Unit = println("メッセージ") |
Null |
null 参照を表す型。全ての参照型(AnyRef)のサブタイプ。 |
val n: String = null |
Nothing |
全ての型のサブタイプ。例外発生など、正常に値を返さない式の型。 | def error(message: String): Nothing = throw new RuntimeException(message) |
Any |
全ての型のスーパータイプ。 | val anyVal: Any = 10 |
AnyVal |
全ての値型 (Int, Doubleなど) のスーパータイプ。 | val anyv: AnyVal = 10 |
AnyRef |
全ての参照型 (クラス、トレイトのインスタンス) のスーパータイプ (JavaのObjectに相当)。 | val anyr: AnyRef = "Hello" |
文字列補間 (String Interpolation)
文字列リテラル内で変数や式を簡単に埋め込む機能です。
s
補間子: 変数をそのまま埋め込みます。f
補間子: printf形式のフォーマット指定で埋め込みます。raw
補間子: エスケープシーケンスを解釈せずに埋め込みます。
val name = "太郎"
val age = 30
val height = 175.5
// s補間子
println(s"私の名前は${name}です。年齢は${age}歳です。")
println(s"来年は${age + 1}歳になります。")
// f補間子
println(f"身長は${height}%.1fcmです。") // 小数点以下1桁まで表示
// raw補間子
println(raw"改行文字 \n はそのまま表示されます。")
演算子
Scalaの演算子は、実際にはメソッド呼び出しの糖衣構文です (例: a + b
は a.+(b)
)。
種類 | 演算子 | 説明 |
---|---|---|
算術演算子 | + |
加算 |
- |
減算 | |
* |
乗算 | |
/ |
除算 | |
% |
剰余 | |
比較演算子 | == |
等価 (値の比較, Javaのequals相当) |
!= |
不等価 | |
> |
より大きい | |
< |
より小さい | |
>= |
以上 | |
<= |
以下 | |
参照比較 | eq , ne |
参照が同じかどうかの比較 (Javaの ==, != 相当)。通常は == を使う。 |
論理演算子 | && |
論理積 (AND) |
|| |
論理和 (OR) | |
! |
否定 (NOT) | |
ビット演算子 | & |
ビットAND |
| |
ビットOR | |
^ |
ビットXOR | |
~ |
ビットNOT | |
<< |
左シフト | |
>> |
算術右シフト | |
>>> |
論理右シフト | |
代入演算子 | = |
代入 |
+= |
加算代入 (例: x += 5 は x = x + 5 ) |
|
-= |
減算代入 | |
*= |
乗算代入 | |
/= , %= , etc. |
他の算術・ビット演算子と組み合わせ可能 |
制御構文
プログラムの流れを制御するための構文です。Scalaでは多くの制御構文が式であり、値を返します。
if / else
条件に基づいて処理を分岐します。式なので値を返します。
val score = 75
val result = if (score >= 80) {
"合格" // ブロックの最後の式が値となる
} else if (score >= 60) {
"追試"
} else {
"不合格"
}
println(s"結果: ${result}") // 結果: 追試
val isEmpty = false
// Unitを返す例 (副作用のため)
if (isEmpty) println("リストは空です")
for ループ (for comprehension)
コレクションの要素に対する繰り返し処理や、より複雑なシーケンス処理に使用されます。yield
を使うと新しいコレクションを生成できます。
val numbers = List(1, 2, 3, 4, 5)
// 基本的なループ (Unitを返す)
println("基本的なループ:")
for (n <- numbers) {
println(n)
}
// フィルター付きループ
println("フィルター付きループ (偶数のみ):")
for (n <- numbers if n % 2 == 0) {
println(n)
}
// yieldを使った新しいコレクションの生成
val squaredNumbers = for (n <- numbers) yield n * n
println(s"二乗したリスト: ${squaredNumbers}") // List(1, 4, 9, 16, 25)
// 複数のジェネレータ (flatMapとmapの糖衣構文)
val pairs = for {
i <- 1 to 2 // 1 から 2 までのRange
j <- 'a' to 'b' // 'a' から 'b' までのRange
} yield (i, j)
println(s"組み合わせ: ${pairs}") // Vector((1,a), (1,b), (2,a), (2,b))
// ガード条件付きの複数ジェネレータ
val filteredPairs = for {
i <- 1 to 3
j <- 1 to 3
if i != j // ガード条件
} yield (i, j)
println(s"異なる組み合わせ: ${filteredPairs}") // Vector((1,2), (1,3), (2,1), (2,3), (3,1), (3,2))
while / do-while ループ
特定の条件が満たされている間、処理を繰り返します。副作用のために使われることが多く、値を返しません (Unit)。関数型スタイルではあまり推奨されません。
var i = 0
println("whileループ:")
while (i < 3) {
println(i)
i += 1 // var変数の更新が必要
}
var j = 0
println("do-whileループ:")
do {
println(j)
j += 1
} while (j < 3)
match 式 (パターンマッチ)
値の構造や型に基づいて処理を分岐させる強力な機能です。Javaのswitch文よりもはるかに高機能です。
val value: Any = 5
val description = value match {
case 1 => "一つ"
case "hello" => "挨拶"
case x: Int if x > 10 => s"10より大きい整数: ${x}" // 型とガード条件
case x: Int => s"その他の整数: ${x}"
case _: Double => "浮動小数点数" // 変数束縛しない場合は _
case list: List[String] => s"文字列のリスト: ${list.mkString(", ")}"
case Some(content) => s"Optionの値: ${content}" // Optionのマッチ
case None => "値なし (None)"
case (a, b) => s"タプル: (${a}, ${b})" // タプルのマッチ
case _ => "その他の型" // デフォルトケース (必須ではないが、網羅的でない場合に警告が出る)
}
println(description) // その他の整数: 5
// コレクションのパターンマッチ
val list = List(1, 2, 3)
list match {
case Nil => println("空のリスト")
case head :: tail => println(s"先頭: ${head}, 残り: ${tail}") // 先頭: 1, 残り: List(2, 3)
case List(x, y, z) => println(s"3要素のリスト: ${x}, ${y}, ${z}")
case _ => println("その他のリスト")
}
コレクション API 📦
Scalaのコレクションライブラリは非常に豊富で強力です。不変 (Immutable) と 可変 (Mutable) の両方のコレクションを提供します。デフォルトは不変コレクションです。
主な不変 (Immutable) コレクション
一度作成すると変更できないコレクションです。スレッドセーフであり、関数型プログラミングに適しています。
List
: 線形シーケンス。先頭への要素追加(::
)やアクセスが高速。Vector
: 汎用的なシーケンス。ランダムアクセス、更新、先頭/末尾への追加/削除が効率的。List
より一般的に推奨されます。Set
: 重複しない要素の集合。要素の存在確認が高速。Map
: キーと値のペアの集合。キーによる値の検索が高速。Seq
: 順序付きシーケンスの汎用トレイト。List
やVector
のスーパータイプ。Range
: 整数の範囲を表すシーケンス。メモリ効率が良い。Tuple
: 固定長の異なる型の要素の組。(例:(1, "Scala", true)
)
// List
val list1 = List(1, 2, 3)
val list2 = 0 :: list1 // 先頭に追加: List(0, 1, 2, 3)
val list3 = list1 :+ 4 // 末尾に追加: List(1, 2, 3, 4) (効率は良くない)
val list4 = list1 ::: List(4, 5) // 結合: List(1, 2, 3, 4, 5)
// Vector
val vec1 = Vector(1, 2, 3)
val vec2 = 0 +: vec1 // 先頭に追加: Vector(0, 1, 2, 3)
val vec3 = vec1 :+ 4 // 末尾に追加: Vector(1, 2, 3, 4) (効率的)
val vec4 = vec1 ++ Vector(4, 5) // 結合: Vector(1, 2, 3, 4, 5)
println(s"Vectorの要素アクセス: ${vec1(1)}") // 2 (高速)
// Set
val set1 = Set(1, 2, 3, 2) // Set(1, 2, 3) - 重複は無視される
val set2 = set1 + 4 // Set(1, 2, 3, 4)
val hasTwo = set1.contains(2) // true
// Map
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3) // Map(a -> 1, b -> 2, c -> 3)
val valueA = map1.get("a") // Some(1) - Option型で返る
val valueD = map1.getOrElse("d", 0) // 0 - キーが存在しない場合のデフォルト値
// Range
val range1 = 1 to 5 // 1, 2, 3, 4, 5 (Inclusive)
val range2 = 1 until 5 // 1, 2, 3, 4 (Exclusive)
val range3 = 1 to 10 by 2 // 1, 3, 5, 7, 9 (Step)
// Tuple
val tuple1 = (1, "Scala", true)
println(s"タプルの1番目の要素: ${tuple1._1}") // 1
val (num, lang, bool) = tuple1 // 分解代入
println(lang) // Scala
主な可変 (Mutable) コレクション
作成後に要素を追加、変更、削除できるコレクションです。scala.collection.mutable
パッケージにあります。使用する際はスレッドセーフティに注意が必要です。
ArrayBuffer
: 可変長の配列。末尾への要素追加が高速。ListBuffer
:List
の可変版。効率的な要素追加が可能で、最後に不変のList
に変換(toList
)できます。mutable.Set
: 可変のセット。mutable.Map
: 可変のマップ。Array
: 固定長の配列 (Javaの配列と互換性あり)。
import scala.collection.mutable // 可変コレクションを使うにはインポートが必要
// ArrayBuffer
val buf = mutable.ArrayBuffer[Int]()
buf += 1 // buf is ArrayBuffer(1)
buf += (2, 3) // buf is ArrayBuffer(1, 2, 3)
buf ++= List(4, 5) // buf is ArrayBuffer(1, 2, 3, 4, 5)
buf(0) = 10 // 要素の更新: ArrayBuffer(10, 2, 3, 4, 5)
buf -= 3 // 要素の削除: ArrayBuffer(10, 2, 4, 5)
val immutableList = buf.toList // 不変Listに変換
// ListBuffer
val listBuf = mutable.ListBuffer[String]()
listBuf += "Hello"
listBuf += "World"
val finalImmutableList = listBuf.toList // List("Hello", "World")
// mutable.Map
val mutableMap = mutable.Map[String, Int]()
mutableMap("one") = 1 // 要素の追加/更新
mutableMap += ("two" -> 2)
mutableMap.remove("one") // 要素の削除
// Array (固定長)
val arr = Array(1, 2, 3)
arr(0) = 10 // 要素の更新
// arr += 4 // これはエラー (サイズ変更不可)
println(arr.mkString(", ")) // 10, 2, 3
共通のコレクション操作
不変・可変コレクションに共通して使える高階関数が多数用意されています。
メソッド | 説明 | 例 (List(1, 2, 3) に対して) |
---|---|---|
map |
各要素に関数を適用し、新しいコレクションを返す。 | list.map(_ * 2) // List(2, 4, 6) |
flatMap |
各要素に関数を適用し、結果のコレクションをフラットにして新しいコレクションを返す。 | list.flatMap(x => List(x, x * 10)) // List(1, 10, 2, 20, 3, 30) |
filter |
条件を満たす要素のみを含む新しいコレクションを返す。 | list.filter(_ % 2 != 0) // List(1, 3) |
filterNot |
条件を満たさない要素のみを含む新しいコレクションを返す。 | list.filterNot(_ % 2 != 0) // List(2) |
foreach |
各要素に関数を適用する (副作用のため)。Unitを返す。 | list.foreach(println) |
find |
条件を満たす最初の要素をOption で返す。 |
list.find(_ > 1) // Some(2) |
exists |
条件を満たす要素が1つでも存在するかどうか (Boolean)。 | list.exists(_ == 3) // true |
forall |
全ての要素が条件を満たすかどうか (Boolean)。 | list.forall(_ < 5) // true |
foldLeft |
初期値とコレクションの要素を使って左から畳み込み演算を行う。 | list.foldLeft(0)(_ + _) // 6 (0+1+2+3) |
foldRight |
初期値とコレクションの要素を使って右から畳み込み演算を行う。 | list.foldRight(0)(_ + _) // 6 (1+(2+(3+0))) |
reduceLeft |
foldLeft に似ているが、初期値なしで最初の要素から始める。空のコレクションでは例外。 |
list.reduceLeft(_ + _) // 6 |
reduceRight |
foldRight に似ているが、初期値なしで最後の要素から始める。空のコレクションでは例外。 |
list.reduceRight(_ + _) // 6 |
sum |
数値コレクションの合計値を計算する。 | list.sum // 6 |
product |
数値コレクションの積を計算する。 | list.product // 6 |
min |
コレクションの最小値を返す。 | list.min // 1 |
max |
コレクションの最大値を返す。 | list.max // 3 |
size / length |
要素数を返す。 | list.size // 3 |
isEmpty / nonEmpty |
コレクションが空かどうかを返す。 | list.isEmpty // false |
head / headOption |
最初の要素を返す (head は空だと例外、headOption はOption )。 |
list.head // 1 , list.headOption // Some(1) |
tail |
最初の要素を除いた残りのコレクションを返す。 | list.tail // List(2, 3) |
last / lastOption |
最後の要素を返す (last は空だと例外、lastOption はOption )。 |
list.last // 3 , list.lastOption // Some(3) |
init |
最後の要素を除いた残りのコレクションを返す。 | list.init // List(1, 2) |
take |
先頭から指定された数の要素を取り出す。 | list.take(2) // List(1, 2) |
drop |
先頭から指定された数の要素を削除する。 | list.drop(1) // List(2, 3) |
slice |
指定された範囲の要素を取り出す。 | list.slice(1, 3) // List(2, 3) |
distinct |
重複する要素を取り除いたコレクションを返す。 | List(1, 2, 2, 3, 1).distinct // List(1, 2, 3) |
sorted |
要素を自然順序でソートしたコレクションを返す。 | List(3, 1, 2).sorted // List(1, 2, 3) |
sortBy |
指定したキーに基づいてソートしたコレクションを返す。 | List("apple", "banana", "kiwi").sortBy(_.length) // List(kiwi, apple, banana) |
groupBy |
指定した関数に基づいて要素をグループ化し、キーとコレクションのMapを返す。 | list.groupBy(_ % 2 == 0) // Map(false -> List(1, 3), true -> List(2)) |
zip |
2つのコレクションをペアのコレクションにまとめる。短い方に長さが合う。 | list.zip(List("a", "b")) // List((1,a), (2,b)) |
mkString |
コレクションの要素を文字列に変換する。区切り文字などを指定可能。 | list.mkString(", ") // "1, 2, 3" |
関数とメソッド
Scalaでは関数は第一級オブジェクトであり、変数に代入したり、引数として渡したり、戻り値として返すことができます。
メソッド定義 (def)
クラスやオブジェクトのメンバーとして定義される関数。
object MyMath {
// 引数と戻り値の型を明示
def add(x: Int, y: Int): Int = {
x + y
}
// 戻り値の型推論も可能 (再帰関数以外)
def subtract(x: Int, y: Int) = x - y
// Unitを返すメソッド (副作用)
def printSum(x: Int, y: Int): Unit = {
println(s"${x} + ${y} = ${add(x, y)}")
}
// デフォルト引数
def greet(name: String = "Guest"): Unit = {
println(s"Hello, ${name}!")
}
greet() // Hello, Guest!
greet("Alice") // Hello, Alice!
// 可変長引数
def sumAll(numbers: Int*): Int = {
numbers.sum
}
sumAll(1, 2, 3) // 6
sumAll() // 0
}
関数リテラル (匿名関数 / ラムダ式)
名前を持たない関数。変数に代入したり、高階関数の引数として渡したりします。
// 型を明示した関数リテラル
val increment: Int => Int = (x: Int) => x + 1
// 型推論を利用した関数リテラル
val multiply = (x: Int, y: Int) => x * y
// プレースホルダ構文 (引数が1回ずつ使われる場合)
val double = (_: Int) * 2
val add = (_: Int) + (_: Int)
// 高階関数への適用
val numbers = List(1, 2, 3)
val squared = numbers.map(x => x * x) // List(1, 4, 9)
val evens = numbers.filter(_ % 2 == 0) // List(2)
println(increment(5)) // 6
println(multiply(3, 4)) // 12
println(double(5)) // 10
println(add(3, 4)) // 7
高階関数 (Higher-Order Functions)
関数を引数として受け取るか、関数を戻り値として返す関数。
// 関数を引数として受け取る関数
def operate(x: Int, y: Int, f: (Int, Int) => Int): Int = {
f(x, y)
}
val sumResult = operate(5, 3, (a, b) => a + b) // 8
val productResult = operate(5, 3, _ * _) // 15
// 関数を返す関数
def multiplier(factor: Int): Int => Int = {
(x: Int) => x * factor
}
val doubler = multiplier(2) // doubler は Int => Int 型の関数
val tripler = multiplier(3) // tripler は Int => Int 型の関数
println(doubler(10)) // 20
println(tripler(10)) // 30
部分適用関数 (Partially Applied Functions)
関数の引数の一部だけを適用して、残りの引数を取る新しい関数を作成すること。
def log(date: java.util.Date, level: String, message: String): Unit = {
println(s"$date [$level] $message")
}
val now = new java.util.Date()
// date引数を固定した新しい関数を作成
val logNow: (String, String) => Unit = log(now, _, _)
logNow("INFO", "処理を開始します")
logNow("WARN", "メモリ使用量が増加しています")
// プレースホルダを使わずに部分適用 (より明確)
val infoLogNow: String => Unit = log(now, "INFO", _)
infoLogNow("データベースに接続しました")
カリー化 (Currying)
複数の引数を取る関数を、引数を1つずつ取る関数の連鎖に変換すること。
// カリー化された関数定義 (引数リストを複数持つ)
def curriedAdd(x: Int)(y: Int): Int = x + y
// 呼び出し方
val result1 = curriedAdd(5)(3) // 8
// 部分適用と似た使い方が可能
val addFive: Int => Int = curriedAdd(5) // y: Int => 5 + y
val result2 = addFive(10) // 15
// 通常の関数をカリー化する .curried メソッド
def multiply(x: Int, y: Int): Int = x * y
val curriedMultiply: Int => Int => Int = (multiply _).curried
val multiplyByTwo: Int => Int = curriedMultiply(2)
println(multiplyByTwo(7)) // 14
クラス、オブジェクト、トレイト
Scalaのオブジェクト指向の構成要素です。
クラス (class)
オブジェクトの設計図。フィールド(状態)とメソッド(振る舞い)を持ちます。
class Person(val name: String, var age: Int) { // プライマリコンストラクタ
// フィールド (valはgetterのみ、varはgetter/setterを自動生成)
// name: String (valなのでgetterのみ)
// age: Int (varなのでgetter/setterあり)
// 補助コンストラクタ (プライマリコンストラクタを呼び出す必要あり)
def this(name: String) = {
this(name, 0) // プライマリコンストラクタ呼び出し
println("補助コンストラクタが呼ばれました")
}
// メソッド
def greet(): Unit = {
println(s"こんにちは、${name}です。${age}歳です。")
}
// プライベートメンバー
private val id: Int = Person.newId() // コンパニオンオブジェクトのメソッドを呼ぶ
override def toString: String = s"Person($name, $age)"
}
// Personクラスのコンパニオンオブジェクト
object Person {
private var counter = 0
private def newId(): Int = {
counter += 1
counter
}
}
// インスタンス化
val person1 = new Person("アリス", 30)
person1.greet() // こんにちは、アリスです。30歳です。
person1.age = 31 // varなので変更可能
println(person1.age) // 31
// person1.name = "ボブ" // valなのでエラー
val person2 = new Person("ボブ") // 補助コンストラクタ呼び出し
person2.greet() // こんにちは、ボブです。0歳です。
オブジェクト (object)
シングルトンオブジェクト。クラスを定義せずにインスタンスを1つだけ作成します。静的メソッドや定数の置き場所としてよく使われます。
object Logger {
def info(message: String): Unit = {
println(s"[INFO] $message")
}
def warn(message: String): Unit = {
println(s"[WARN] $message")
}
}
// objectのメンバーは直接アクセス可能
Logger.info("アプリケーション起動")
クラスと同じ名前を持つobject
をコンパニオンオブジェクトと呼びます。クラスのprivate
メンバーにアクセスできます。
トレイト (trait)
Javaのインターフェースに似ていますが、メソッドの実装やフィールドを持つことができます。クラスにミックスイン(多重継承のように)して機能を追加します。
trait Greeter {
def greet(name: String): Unit // 抽象メソッド
}
trait Logger {
// 実装を持つメソッド
def log(message: String): Unit = {
println(s"[LOG] $message")
}
}
// トレイトをミックスインするクラス
class Service(serviceName: String) extends Greeter with Logger {
// Greeterトレイトの抽象メソッドを実装
override def greet(name: String): Unit = {
log(s"Greeting $name from $serviceName") // Loggerトレイトのメソッドを使用
println(s"Hello, $name! Welcome to $serviceName.")
}
}
val service = new Service("MyAwesomeService")
service.greet("Scalaユーザー")
service.log("Service initialized.")
ケースクラス (case class)
データ保持を主目的とした特別なクラス。コンパイラが便利なメソッド(equals
, hashCode
, toString
, copy
, コンパニオンオブジェクトのapply
/unapply
など)を自動生成します。
- デフォルトで不変 (immutable)。
- パターンマッチに最適化されている。
new
なしでインスタンス化可能 (apply
メソッド)。
case class Point(x: Double, y: Double)
// new なしでインスタンス化
val p1 = Point(1.0, 2.0)
val p2 = Point(1.0, 2.0)
val p3 = Point(3.0, 4.0)
// equalsが自動生成 (内容で比較)
println(p1 == p2) // true
println(p1 == p3) // false
// toStringが自動生成
println(p1) // Point(1.0,2.0)
// copyメソッドで一部を変更した新しいインスタンスを作成
val p4 = p1.copy(y = 5.0)
println(p4) // Point(1.0,5.0)
// パターンマッチで利用 (unapplyメソッドが使われる)
def matchPoint(point: Point): String = point match {
case Point(0, 0) => "Origin"
case Point(x, 0) => s"On X axis at $x"
case Point(0, y) => s"On Y axis at $y"
case Point(x, y) => s"Point at ($x, $y)"
}
println(matchPoint(p1)) // Point at (1.0, 2.0)
println(matchPoint(Point(0, 5))) // On Y axis at 5.0
抽象クラス (abstract class)
インスタンス化できないクラス。抽象メソッド(実装のないメソッド)や抽象フィールドを持つことができます。トレイトとは異なり、コンストラクタ引数を取ることができます。
abstract class Shape(name: String) { // コンストラクタ引数を取れる
def area: Double // 抽象メソッド
def description: String = s"This is a $name" // 実装を持つメソッド
}
class Circle(radius: Double) extends Shape("Circle") {
override def area: Double = Math.PI * radius * radius
}
class Rectangle(width: Double, height: Double) extends Shape("Rectangle") {
override def area: Double = width * height
}
val circle = new Circle(5.0)
println(circle.description) // This is a Circle
println(circle.area) // 78.539...
// val shape = new Shape("Abstract") // エラー: 抽象クラスはインスタンス化できない
パッケージとインポート
コードを整理し、名前空間の衝突を避けるための仕組みです。
パッケージ宣言 (package)
ファイルの先頭で、そのファイル内のコードが属するパッケージを指定します。
package com.example.myapp.utils
class StringUtils {
// ...
}
ネストしたパッケージ宣言も可能です。
package com.example {
package myapp {
package utils {
class DateUtils {
// ...
}
}
class MainApp {
val dateUtil = new utils.DateUtils() // 同じmyappパッケージ内のutilsを参照
}
}
}
インポート (import)
他のパッケージのクラスやオブジェクトなどを現在のスコープで使用できるようにします。
package com.example.myapp
// 特定のクラスをインポート
import java.util.Date
import scala.collection.mutable.ListBuffer
// パッケージ内の全てのメンバーをインポート
import scala.concurrent._ // Future, Promise, ExecutionContextなど
// 複数のメンバーを選択してインポート
import java.io.{File, PrintWriter}
// 名前を変更してインポート (エイリアス)
import java.util.{HashMap => JavaHashMap}
// 特定のメンバーを除外してインポート (Scala 2.13以降)
// import scala.collection.mutable.{Set => _, _} // Setを除外し、他を全てインポート
class DataProcessor {
val now = new Date()
val buffer = ListBuffer[String]()
// val futureResult: Future[Int] = ??? (Futureを使用可能)
val file = new File("data.txt")
val javaMap = new JavaHashMap[String, Int]()
// val mutableSet = mutable.Set() // エイリアスまたは除外がない場合はエラーの可能性
}
インポートはファイルの先頭だけでなく、必要なスコープ(クラス定義内、メソッド定義内など)で行うことも可能です。
例外処理
エラー発生時の処理を記述するための構文です。Javaと同様のtry-catch-finally
ブロックを使用します。
import java.io.{FileReader, FileNotFoundException, IOException}
def readFile(path: String): Option[String] = {
var reader: Option[FileReader] = None
try {
reader = Some(new FileReader(path))
// ファイル読み込み処理 (ここでは省略)
val content = "ファイルの内容..." // 仮
Some(content)
} catch {
// パターンマッチを使って例外を捕捉
case ex: FileNotFoundException =>
println(s"エラー: ファイルが見つかりません - ${path}")
None
case ex: IOException =>
println(s"エラー: I/Oエラーが発生しました - ${ex.getMessage}")
None
// 必要に応じて他の例外ケースを追加
case ex: Exception => // より一般的な例外
println(s"予期せぬエラー: ${ex.getMessage}")
throw ex // キャッチして再スローする場合
} finally {
// リソースの解放など、常に実行したい処理
println("finallyブロックを実行します。")
reader.foreach(_.close()) // Optionを使って安全にcloseを呼び出す
}
}
// 例外処理の呼び出し
val contentOption = readFile("non_existent_file.txt")
contentOption match {
case Some(content) => println("ファイル読み込み成功")
case None => println("ファイル読み込み失敗")
}
// 例外を発生させる (throw)
def checkAge(age: Int): Unit = {
if (age < 0) {
throw new IllegalArgumentException("年齢は負の値にできません")
}
println(s"年齢は ${age}歳です")
}
try {
checkAge(-5)
} catch {
case e: IllegalArgumentException => println(s"捕捉した例外: ${e.getMessage}")
}
Scalaでは、Try
, Option
, Either
といった型を使って、例外処理をより関数型スタイルで行うことが推奨される場合が多いです。
型パラメータ (ジェネリクス)
クラス、トレイト、メソッドを様々な型に対して汎用的に使えるようにする仕組みです。
基本的な使い方
型パラメータは角括弧 []
内に記述します。
// 型パラメータを持つクラス
class Box[T](content: T) {
def getContent: T = content
def map[U](f: T => U): Box[U] = new Box(f(content))
}
val intBox = new Box[Int](10) // 型を明示
val stringBox = new Box("Hello") // 型推論も可能
val doubledContent = intBox.map(_ * 2).getContent // 20
val upperCaseContent = stringBox.map(_.toUpperCase).getContent // "HELLO"
// 型パラメータを持つメソッド
def getMiddle[T](list: List[T]): Option[T] = {
if (list.isEmpty) None else Some(list(list.size / 2))
}
val middleInt = getMiddle(List(1, 2, 3, 4, 5)) // Some(3)
val middleString = getMiddle(List("a", "b", "c")) // Some(b)
変位指定 (Variance)
型パラメータを持つ型同士のサブタイプ関係を制御します。
- 共変 (Covariant)
[+T]
:A <: B
ならばC[A] <: C[B]
。主に不変コレクションや「生産者」の役割を持つ型に使われます。(例:List[+A]
) - 反変 (Contravariant)
[-T]
:A <: B
ならばC[B] <: C[A]
。主に関数型や「消費者」の役割を持つ型に使われます。(例: 関数Function1[-T1, +R]
) - 非変 (Invariant)
[T]
: サブタイプ関係を継承しません。デフォルト。可変コレクションなど。(例:Array[T]
,mutable.Buffer[T]
)
class Animal
class Dog extends Animal
class Cat extends Animal
// 共変の例: List[Dog] は List[Animal] のサブタイプ
val dogs: List[Dog] = List(new Dog(), new Dog())
val animals: List[Animal] = dogs // OK
// 非変の例: Array[Dog] は Array[Animal] のサブタイプではない
val dogArray: Array[Dog] = Array(new Dog())
// val animalArray: Array[Animal] = dogArray // コンパイルエラー! 💥
// 反変の例: Animal => String は Dog => String のサブタイプ
val printAnimal: Animal => String = (a: Animal) => s"Animal: ${a.getClass.getSimpleName}"
val printDog: Dog => String = printAnimal // OK
// val printSpecific: Dog => String = (d: Dog) => "Just a dog"
// val printGeneral: Animal => String = printSpecific // コンパイルエラー! 💥
class MyContainer[+A](value: A) // 共変
class MyProcessor[-A] { def process(a: A): Unit = println(a) } // 反変
class MyBuffer[A] // 非変
val containerDog: MyContainer[Dog] = new MyContainer(new Dog())
val containerAnimal: MyContainer[Animal] = containerDog // OK
val processorAnimal: MyProcessor[Animal] = new MyProcessor[Animal]
val processorDog: MyProcessor[Dog] = processorAnimal // OK
型境界 (Type Bounds)
型パラメータに制約を設けます。
- 上限境界 (Upper Bound)
T <: SuperType
: TはSuperTypeまたはそのサブタイプである必要があります。 - 下限境界 (Lower Bound)
T >: SubType
: TはSubTypeまたはそのスーパータイプである必要があります。 - ビュー境界 (View Bound)
T <% BoundType
(非推奨、implicit conversionを使用): TからBoundTypeへの暗黙の型変換が存在する必要があります。 - コンテキスト境界 (Context Bound)
T : Context
(推奨、type classパターンで使用): Context[T] 型の暗黙の値が存在する必要があります。
// 上限境界: TはNumberのサブタイプである必要がある
def processNumber[T <: Number](num: T): Double = {
num.doubleValue() // Numberクラスのメソッドが使える
}
processNumber(10) // IntはNumberのサブタイプ (実際は暗黙変換経由)
processNumber(3.14) // DoubleはNumberのサブタイプ
// 下限境界: UはTのスーパータイプである必要がある (共変と組み合わせて使われることが多い)
class LinkedList[+T] {
// prependは新しい要素Bを追加するが、Bは元の型Tのスーパータイプでも良い (B >: T)
// 結果のリストはLinkedList[B]型になる
def prepend[U >: T](elem: U): LinkedList[U] = ??? // 実装省略
}
// コンテキスト境界 (Ordering[T] の暗黙の値が必要)
// これは def sort[T](list: List[T])(implicit ord: Ordering[T]) と同等
def sort[T : Ordering](list: List[T]): List[T] = {
list.sorted // sortedメソッドは暗黙のOrderingを要求する
}
sort(List(3, 1, 2)) // List(1, 2, 3) - Intには暗黙のOrderingが存在する
sort(List("c", "a", "b")) // List(a, b, c) - Stringにも存在する
// カスタムクラスで試す (Orderingが必要)
case class User(name: String, age: Int)
// implicit val userOrdering: Ordering[User] = Ordering.by(_.age) // 年齢でソートするOrderingを定義
// sort(List(User("Bob", 30), User("Alice", 25))) // これでソート可能になる
Implicits (暗黙の仕組み) ✨
Scalaの強力な機能の一つですが、使いすぎるとコードが読みにくくなるため注意が必要です。コンパイラがコードの隙間を「暗黙的に」埋めてくれます。
Scala 3 では Implicits の構文が大幅に変更され、より明示的な given
/ using
構文が導入されました。以下の例は主に Scala 2 の構文に基づいています。
暗黙のパラメータ (Implicit Parameters)
メソッドの最後のパラメータリストをimplicit
としてマークすると、コンパイラはスコープ内から適切な型の暗黙の値を探して自動的に渡します。
import scala.concurrent.ExecutionContext
// ExecutionContextを暗黙のパラメータとして受け取る関数
def runAsyncTask(task: () => Unit)(implicit ec: ExecutionContext): Unit = {
ec.execute(() => task())
}
// スコープ内に暗黙のExecutionContextを定義 (通常はライブラリが提供)
implicit val globalEc: ExecutionContext = scala.concurrent.ExecutionContext.global
// 呼び出し時には ec を明示的に渡す必要がない
runAsyncTask(() => println("非同期タスク実行中..."))
コンテキスト境界 [T : Context]
は、暗黙のパラメータ (implicit ctx: Context[T])
の糖衣構文です。
暗黙の型変換 (Implicit Conversions)
ある型から別の型へコンパイラが自動的に変換するルールを定義します。implicit def
を使います。非常に強力ですが、乱用するとコードの挙動が予測困難になる可能性があります。
import scala.language.implicitConversions // 機能を使うために推奨されるインポート
case class Rational(n: Int, d: Int) {
require(d != 0)
// ... 算術演算などを定義 ...
override def toString: String = s"$n/$d"
}
// IntからRationalへの暗黙の変換
implicit def intToRational(x: Int): Rational = Rational(x, 1)
val r1 = Rational(1, 2)
// 暗黙の変換が適用され、 3 は Rational(3, 1) に変換される
// val result = r1 + 3 // Rationalクラスに+演算子が適切に定義されていると仮定
println(s"IntからRationalへの暗黙変換: 5 は ${5: Rational}") // 5 は Rational(5,1)
暗黙のクラス (Implicit Classes)
既存のクラスに新しいメソッドを「追加」するように見せる機能(拡張メソッド)。implicit class
を使います。暗黙クラスは特定のルールに従う必要があります(トップレベルまたはオブジェクト内に定義、プライマリコンストラクタは1つの非暗黙パラメータのみ)。
object StringExtensions {
implicit class RichString(val s: String) extends AnyVal { // AnyValを継承すると効率が良い場合がある
def capitalizeWords: String = {
s.split(" ").map(_.capitalize).mkString(" ")
}
def isPalindrome: Boolean = {
s == s.reverse
}
}
}
// StringExtensionsをインポートして拡張メソッドを使う
import StringExtensions._
val message = "hello scala world"
println(message.capitalizeWords) // Hello Scala World
val palindrome = "madam"
println(s"'${palindrome}' is palindrome: ${palindrome.isPalindrome}") // true
型クラス (Type Classes) パターン
Implicits を利用した重要なデザインパターン。アドホック多相(特定の型に対して後から操作を追加する)を実現します。主に以下の3要素で構成されます。
- 型クラス・トレイト: 共通の操作(メソッド)を定義するジェネリックなトレイト。
- 型クラス・インスタンス: 特定の型に対する型クラス・トレイトの実装。
implicit val
やimplicit object
で定義。 - インターフェース: 型クラスを利用するメソッド。暗黙のパラメータやコンテキスト境界を使って型クラス・インスタンスを受け取る。
// 1. 型クラス・トレイト (Show: 値を文字列で表現する能力)
trait Show[A] {
def show(a: A): String
}
// 2. 型クラス・インスタンス (特定の型に対するShowの実装)
object ShowInstances {
implicit val intShow: Show[Int] = new Show[Int] {
def show(a: Int): String = s"Int: $a"
}
implicit val stringShow: Show[String] = new Show[String] {
def show(a: String): String = s"String: '$a'"
}
// case class に対するインスタンスも定義可能
case class Person(name: String, age: Int)
implicit val personShow: Show[Person] = new Show[Person] {
def show(p: Person): String = s"Person(name=${p.name}, age=${p.age})"
}
}
// 3. インターフェース (Show[A]の暗黙のインスタンスを利用するメソッド)
object ShowSyntax {
// インターフェースメソッド (直接呼び出し)
def printValue[A](a: A)(implicit shower: Show[A]): Unit = {
println(shower.show(a))
}
// 拡張メソッド (より便利)
implicit class ShowOps[A](a: A) {
def show(implicit shower: Show[A]): String = shower.show(a)
def print(implicit shower: Show[A]): Unit = println(shower.show(a))
}
}
// --- 利用例 ---
import ShowInstances._ // 型クラスインスタンスをスコープに入れる
import ShowSyntax._ // インターフェース (拡張メソッド) をスコープに入れる
val myInt = 123
val myString = "Scala"
val person = Person("Alice", 30)
// 直接呼び出し
printValue(myInt) // Int: 123
printValue(myString) // String: 'Scala'
printValue(person) // Person(name=Alice, age=30)
// 拡張メソッド呼び出し
println(myInt.show) // Int: 123
myString.print // String: 'Scala'
person.print // Person(name=Alice, age=30)
Future と並行処理 ⏳
ノンブロッキングな非同期処理を扱うためのコア機能です。scala.concurrent
パッケージにあります。
Future
非同期計算の結果を表すプレースホルダ。計算が完了すると、成功(値を持つ)または失敗(例外を持つ)のいずれかの状態になります。
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global // 暗黙のExecutionContextが必要
// Futureの作成 (非同期に実行されるブロック)
val futureInt: Future[Int] = Future {
println("非同期計算を開始...")
Thread.sleep(1000) // 時間のかかる処理をシミュレート
if (math.random > 0.1) {
42 // 成功時の値
} else {
throw new RuntimeException("計算に失敗しました!") // 失敗時の例外
}
}
println("Futureを作成しました。結果を待たずに次の処理へ...")
// コールバックによる結果の処理 (ノンブロッキング)
futureInt.onComplete {
case Success(value) => println(s"成功: 結果は ${value} です。")
case Failure(exception) => println(s"失敗: エラーは ${exception.getMessage} です。")
}
// map, flatMapなどを使ったFutureの合成 (ノンブロッキング)
val futureString: Future[String] = futureInt.map { result =>
s"計算結果は $result"
}
val futureCombined: Future[Int] = futureInt.flatMap { r1 =>
Future {
println("次の非同期計算を開始...")
Thread.sleep(500)
r1 * 2
}
}
// Futureの結果を同期的に待機 (注意: ブロッキング処理。メインスレッド等での使用は避ける)
// REPLやテスト、アプリケーションのシャットダウン時などに限定的に使う
try {
val result = Await.result(futureInt, 5.seconds) // タイムアウト付きで待機
println(s"Awaitで取得した結果: $result")
} catch {
case e: Exception => println(s"Await中にエラー: ${e.getMessage}")
}
println("メインスレッドの処理は継続...")
Thread.sleep(3000) // 非同期処理が終わるのを待つため (サンプル用)
ExecutionContext
Future内の処理をどのスレッドプールで実行するかを決定します。通常は implicit
パラメータとして渡されます。scala.concurrent.ExecutionContext.Implicits.global
はデフォルトのスレッドプールを提供しますが、用途に応じて専用のスレッドプールを作成することが推奨されます。
Promise
Futureを手動で完了させるためのオブジェクト。Future
が読み取り専用のプレースホルダであるのに対し、Promise
は書き込み可能な端点です。promise.future
で関連するFutureを取得できます。
import scala.concurrent.Promise
import scala.concurrent.ExecutionContext.Implicits.global
// Promiseを作成
val promiseString = Promise[String]()
val futureFromStringPromise: Future[String] = promiseString.future
// Futureの結果を処理するコールバックを登録
futureFromStringPromise.onComplete {
case Success(value) => println(s"PromiseからのFuture成功: ${value}")
case Failure(ex) => println(s"PromiseからのFuture失敗: ${ex.getMessage}")
}
// 別のスレッドやイベントハンドラなどでPromiseを完了させる
Future {
Thread.sleep(1500)
if (math.random > 0.5) {
promiseString.success("Promiseが正常に完了しました!")
} else {
promiseString.failure(new Exception("Promiseの処理中にエラーが発生"))
}
}
Thread.sleep(2000) // Promiseが完了するのを待つ (サンプル用)
Futureの合成関数
複数のFutureを組み合わせて処理するための便利な関数があります。
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Success
// 複数のFutureを並行実行し、全ての結果を待つ (結果は元の順序でSeqになる)
val f1 = Future { Thread.sleep(100); 1 }
val f2 = Future { Thread.sleep(50); "two" }
val f3 = Future { Thread.sleep(200); true }
val allFutures: Future[Seq[Any]] = Future.sequence(List(f1, f2, f3))
allFutures.foreach(results => println(s"Future.sequenceの結果: $results")) // List(1, two, true)
// 最初に完了したFutureの結果を取得する
val firstCompleted: Future[Any] = Future.firstCompletedOf(List(f1, f2, f3))
firstCompleted.foreach(result => println(s"Future.firstCompletedOfの結果: $result")) // "two" が出力される可能性が高い
// Futureの結果を畳み込む (シーケンシャルに実行されるわけではない)
val numbers = List(1, 2, 3, 4)
val futureSum: Future[Int] = Future.foldLeft(numbers.map(i => Future { Thread.sleep(i*50L); i }))(0)(_ + _)
futureSum.foreach(sum => println(s"Future.foldLeftの結果: $sum")) // 10
// Futureの結果をトラバース (コレクションの各要素に関数を適用してFutureを生成し、それらをsequenceする)
val futureStrings: Future[List[String]] = Future.traverse(List(1, 2, 3))(i => Future { s"Item $i" })
futureStrings.foreach(list => println(s"Future.traverseの結果: $list")) // List(Item 1, Item 2, Item 3)
エラーハンドリング (Option, Either, Try)
例外だけでなく、より関数的な方法でエラーや欠損値を扱うための型です。
Option[T]
値が存在するかもしれないし、しないかもしれない状況を表します。null
の使用を避けるのに役立ちます。
Some[T]
: 値が存在する場合。None
: 値が存在しない場合。
val maybeName: Option[String] = Some("Alice")
val noName: Option[String] = None
// パターンマッチで処理
def greet(optName: Option[String]): String = optName match {
case Some(name) => s"Hello, $name!"
case None => "Hello, Guest!"
}
println(greet(maybeName)) // Hello, Alice!
println(greet(noName)) // Hello, Guest!
// map, flatMap, filter などで安全に操作
val upperName = maybeName.map(_.toUpperCase) // Some("ALICE")
val nameLength = maybeName.map(_.length) // Some(5)
val noLength = noName.map(_.length) // None
// getOrElseでデフォルト値を取得
val actualName = maybeName.getOrElse("Default User") // Alice
val defaultName = noName.getOrElse("Default User") // Default User
// for comprehension でネストしたOptionを安全に扱う
val maybeFirstName = Some("Bob")
val maybeLastName = Some("Martin")
val maybeAge = None
val fullName = for {
fname <- maybeFirstName
lname <- maybeLastName
// age <- maybeAge // これがあると結果は None になる
} yield s"$fname $lname"
println(fullName) // Some(Bob Martin)
Either[A, B]
2つの可能性のある型のどちらか一方の値を持つことを表します。慣例的に、左側 (Left[A]
) にエラーや失敗の情報を、右側 (Right[B]
) に成功した場合の値を格納します。
// 右側が成功 (Right-biased by convention)
def divide(x: Double, y: Double): Either[String, Double] = {
if (y == 0) Left("ゼロ除算エラー!")
else Right(x / y)
}
val result1 = divide(10, 2) // Right(5.0)
val result2 = divide(10, 0) // Left("ゼロ除算エラー!")
// パターンマッチで処理
result1 match {
case Right(value) => println(s"成功: 結果 = $value")
case Left(error) => println(s"失敗: エラー = $error")
}
result2 match {
case Right(value) => println(s"成功: 結果 = $value")
case Left(error) => println(s"失敗: エラー = $error")
}
// map, flatMap は Right の値に対してのみ適用される (Right-biased)
val squaredResult = result1.map(v => v * v) // Right(25.0)
val errorResult = result2.map(v => v * v) // Left("ゼロ除算エラー!") (変化なし)
// getOrElse でデフォルト値を取得 (Rightの場合のみ値を取得)
val valueOrDefault = result1.getOrElse(0.0) // 5.0
val errorOrDefault = result2.getOrElse(0.0) // 0.0
// for comprehension (Right の場合のみ進行)
val calculation = for {
r1 <- divide(20, 2) // Right(10.0)
r2 <- divide(r1, 5) // Right(2.0)
r3 <- divide(r2, 0) // Left(...) ここで止まる
} yield r3 * 10
println(calculation) // Left(ゼロ除算エラー!)
Try[T]
例外が発生する可能性のある計算の結果を表します。scala.util.Try
。
Success[T]
: 計算が成功し、結果の値を持つ。Failure[T]
: 計算中に例外が発生し、その例外 (Throwable
) を持つ。
import scala.util.{Try, Success, Failure}
def parseInt(s: String): Try[Int] = Try {
s.toInt // このブロック内で例外が発生すると Failure になる
}
val successParse = parseInt("123") // Success(123)
val failureParse = parseInt("abc") // Failure(java.lang.NumberFormatException: For input string: "abc")
// パターンマッチで処理
successParse match {
case Success(value) => println(s"パース成功: $value")
case Failure(ex) => println(s"パース失敗: ${ex.getMessage}")
}
failureParse match {
case Success(value) => println(s"パース成功: $value")
case Failure(ex) => println(s"パース失敗: ${ex.getMessage}")
}
// map, flatMap は Success の値に対してのみ適用される
val incremented = successParse.map(_ + 1) // Success(124)
val failedIncrement = failureParse.map(_ + 1) // Failure(...) (変化なし)
// recover で Failure を別の値や Success に変換
val recovered = failureParse.recover {
case _: NumberFormatException => 0 // NumberFormatExceptionなら0にする
} // Success(0)
// getOrElse でデフォルト値を取得
val parsedValue = successParse.getOrElse(-1) // 123
val defaultValue = failureParse.getOrElse(-1) // -1
// for comprehension (Success の場合のみ進行)
val computation = for {
num1 <- parseInt("10") // Success(10)
num2 <- parseInt("2") // Success(2)
// num3 <- parseInt("foo") // Failure(...) ここで止まる
} yield num1 / num2
println(computation) // Success(5)
コメント