cv.cm

コーディング · プログラミング言語

Scala入門:初心者向けチュートリアル

約11分で読めます

結論

Scalaは、オブジェクト指向と関数型プログラミングを組み合わせた、JVM上で動く静的型付け言語です。Apache Sparkを使ったデータエンジニアリングや、並行処理を行うバックエンドで広く使われています。主な特長は、型推論、イミュータブルなコレクション、case class、パターンマッチです。このページではScala 3を使い、環境構築、値、制御構文、関数、コレクション、trait、enum、Option・Either・Tryによるエラー処理を解説します。

Scalaとは何か、何に使われるのか

Scalaは、オブジェクト指向と関数型プログラミングを組み合わせた静的型付け言語です。EPFLのMartin Oderskyが開発し、主にJava仮想マシン(JVM)上で動作します。ブラウザ向けのScala.jsや、ネイティブバイナリ向けのScala Nativeもあります。ScalaのコードからはあらゆるJavaライブラリを利用できます。

Scalaは、データエンジニアリング(Apache SparkはScalaで書かれています)、並行・分散バックエンド(Akka、Pekko、ZIO、Cats Effect)、金融やストリーミングのシステムで広く使われています。このチュートリアルでは、インデントによるブロックが使えるScala 3の構文を使います。波括弧も引き続き利用できます。

ScalaのインストールとHello, worldの実行

推奨のインストーラーはCoursier(cs setup)で、scala コマンドとJDKをインストールします。Scala 3では、@main を付けたメソッドがプログラムのエントリーポイントになります。

// hello.scala
@main def hello(): Unit =
  println("Hello, world!")
scala run hello.scala
# Hello, world!

大きなプロジェクトでは、sbtやMillが一般的なビルドツールです。Scastie(scastie.scala-lang.org)を使えば、インストール不要でブラウザ上でScalaを実行できます。

値、変数、基本の型

イミュータブルな値には val、ミュータブルな変数には var を使います。型は推論されますが、コロンの後ろに書くこともできます。慣用的なScalaでは、ほぼすべての場面で val を使います。

val name = "Scala"            // String
var count: Int = 1            // mutable
count += 1
val ratio = 3.0 / 4           // Double: 0.75
val big = 12345678901L        // Long
val ok = count > 1            // Boolean
println(s"$name: count=$count ratio=$ratio ok=$ok")
// Scala: count=2 ratio=0.75 ok=true
println(f"pi is ${math.Pi}%.3f")   // pi is 3.142
型例補足
Int4232ビット符号付き整数
Long42L64ビット符号付き整数
Double3.1464ビット浮動小数点数
Booleantruetrue または false
String"hi"Javaの文字列。s"..." と f"..." の補間子が使える
Unit()void に相当。意味のある値を持たない

Scalaではすべてが式です。if、ブロック、match はいずれも値を返すので、一時的な var が必要になることはほとんどありません。

制御構文とパターンマッチ

if は値を返します。for は範囲やコレクションを走査し、yield を付けると新しいコレクションを作ります。match はScalaの強力なパターンマッチで、この言語を代表する機能です。

val n = 7
val parity = if n % 2 == 0 then "even" else "odd"
println(parity)                        // odd
for i <- 1 to 3 do print(s"$i ")       // 1 2 3
println()
val squares = for i <- 1 to 4 yield i * i
println(squares)                       // Vector(1, 4, 9, 16)
def describe(x: Any): String = x match
  case 0             => "zero"
  case i: Int if i < 0 => "negative int"
  case i: Int        => s"int $i"
  case s: String     => s"string of length ${s.length}"
  case (a, b)        => s"pair $a and $b"
  case _             => "something else"
println(describe(-5))                  // negative int
println(describe("hey"))               // string of length 3
println(describe((1, "x")))            // pair 1 and x

1 to 3 は3を含み、1 until 3 は3の手前で止まります。最後の case _ は何にでもマッチするワイルドカードで、これがないとマッチしない値に対して MatchError が投げられます。

関数と高階関数

メソッドは def で定義します。関数は値なので、変数に保存したり、引数として渡したり、戻り値として返したりできます。引数にはデフォルト値を指定でき、呼び出し側は名前付き引数を使えます。

def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"$greeting, $name!"
val double = (x: Int) => x * 2        // function value
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(add(2, 3))                    // 5
println(greet("Ann"))                 // Hello, Ann!
println(greet("Ann", greeting = "Hi")) // Hi, Ann!
println(applyTwice(double, 5))        // 20
println(List(1, 2, 3, 4).filter(_ % 2 == 0).map(_ * 10)) // List(20, 40)

_ * 10 のアンダースコアは、引数が1つのラムダ x => x * 10 の省略記法です。

コレクション

標準のコレクションはデフォルトでイミュータブルです。map、filter、:+ などの操作は新しいコレクションを返し、元のコレクションは変更しません。ミュータブル版は scala.collection.mutable にあります。

val nums = List(5, 3, 8)
val more = nums :+ 1                  // List(5, 3, 8, 1); nums unchanged
println(more.sorted)                  // List(1, 3, 5, 8)
println(nums.sum)                     // 16
val v = Vector("a", "b", "c")         // fast indexed access
println(v(1))                         // b
val ages = Map("Ann" -> 31, "Bo" -> 25)
println(ages("Ann"))                  // 31
println(ages.get("Zed"))              // None
println(ages.getOrElse("Zed", 0))     // 0
val unique = Set(1, 2, 2, 3)
println(unique.size)                  // 3
val words = List("apple", "avocado", "banana")
println(words.groupBy(_.head)('a'))  // List(apple, avocado)
コレクション用途
Listイミュータブルな連結リスト。先頭への追加と、head・tailへのアクセスが高速
Vectorイミュータブルなインデックス付きシーケンス。汎用的なデフォルトとして適している
Mapキーによる値の検索
Set重複のない要素と所属判定
Arrayミュータブルで固定長のJava配列

case class、trait、enum

case class はイミュータブルなデータ型で、等価性、toString、copy、パターンマッチのサポートが自動的に用意されます。trait は、コードも持てるインターフェースのようなものです。Scala 3では、代数的データ型のための enum が加わりました。

trait Shape:
  def area: Double
case class Circle(r: Double) extends Shape:
  def area: Double = math.Pi * r * r
case class Rect(w: Double, h: Double) extends Shape:
  def area: Double = w * h
enum Color:
  case Red, Green, Blue
@main def shapes(): Unit =
  val all: List[Shape] = List(Circle(1), Rect(2, 3))
  all.foreach(s => println(f"$s area=${s.area}%.2f"))
  // Circle(1.0) area=3.14
  // Rect(2.0,3.0) area=6.00
  val r = Rect(2, 3)
  println(r == Rect(2, 3))            // true
  println(r.copy(h = 10))             // Rect(2.0,10.0)
  println(Color.values.toList)        // List(Red, Green, Blue)

そのほかの代表的な機能に、given と using(コンテキストパラメータ。以前のimplicit)、拡張メソッド、opaque type、Int | String のようなユニオン型があります。

Option、Either、Tryによるエラー処理

Scalaにも try、catch、finally はありますが、慣用的なコードでは失敗を型で表現します。Option は Some(value) か None のどちらかです。Either は Left のエラーか Right の結果を保持します。Try は例外を Failure として捕捉します。

import scala.util.{Try, Success, Failure}
def parseInt(s: String): Option[Int] = s.toIntOption
def safeDivide(a: Int, b: Int): Either[String, Int] =
  if b == 0 then Left("division by zero") else Right(a / b)
println(parseInt("42"))               // Some(42)
println(parseInt("x"))                // None
println(safeDivide(10, 2))            // Right(5)
println(safeDivide(1, 0))             // Left(division by zero)
Try("abc".toInt) match
  case Success(n) => println(s"got $n")
  case Failure(e) => println(s"failed: ${e.getClass.getSimpleName}")
// failed: NumberFormatException

小さな完成プログラム:成績レポート

このプログラムは、name,score 形式の行を解析し、不正な行を読み飛ばして、各生徒の平均点を表示します。case class、Option、groupBy、文字列の書式設定を使っています。

case class Score(name: String, value: Int)
def parse(line: String): Option[Score] =
  line.split(",").map(_.trim) match
    case Array(name, v) => v.toIntOption.map(Score(name, _))
    case _              => None
@main def report(): Unit =
  val lines = List("ann, 90", "bo, 70", "ann, 80", "broken line", "bo, x")
  val scores = lines.flatMap(parse)
  val averages = scores.groupBy(_.name).view.mapValues { list =>
    list.map(_.value).sum.toDouble / list.size
  }.toList.sortBy(_._1)
  for (name, avg) <- averages do
    println(f"$name%-4s $avg%.1f")
// ann  85.0
// bo   70.0

次のステップ

  • docs.scala-lang.orgの「Scala 3 Book」。基礎からコンテキスト抽象化まで、言語全体を扱っています。
  • ファイル、JSON、HTTPなど日常的な作業向けの「Scala Toolkit」。
  • Scalaでデータ処理をしたい場合は、Apache Sparkのドキュメント。
  • scala-lang.orgからリンクされている講座。EPFLの関数型プログラミング講座も含まれます。

よくある質問

Scala 2とScala 3のどちらを学ぶべきですか?

新しいプロジェクトにはScala 3を学びましょう。既存のコードベースでは、Sparkエコシステムの一部を含め、Scala 2.13もまだ多く見られますが、中心となる概念はほとんど同じです。

Scalaは関数型言語ですか?

Scalaはオブジェクト指向であり、関数型でもあります。すべての値がオブジェクトですが、関数はファーストクラスの値で、コレクションはデフォルトでイミュータブルであり、パターンマッチが中心的な役割を果たします。

Scalaのvalとvarの違いは何ですか?

val は再代入できない値を、var は再代入できる変数を定義します。慣用的なScalaでは、val とイミュータブルなコレクションを組み合わせて使います。

case classとは何ですか?

case classは、イミュータブルなデータのためのクラスです。コンパイラが、構造的な等価性、読みやすい toString、copy メソッド、パターンマッチのサポートを生成します。

Scalaは今でも使われていますか?

はい。Apache Spark、ストリーミングやデータ基盤、ZIOやCats Effectなどの関数型バックエンドライブラリで、今でも中核的な言語です。

コーディング講座の一覧