Scala
Scalaは、静的型付けのマルチパラダイムプログラミング言語です。オブジェクト指向プログラミングと関数型プログラミングの特徴を統合的に備え、型安全性と表現力の高さを両立しています。2004年に初版がリリースされ、2021年には大幅な改良が施されたScala 3(Dotty)が登場し、より直感的な構文と強力な型システムを提供しています。
Java仮想マシン(JVM)上で動作するため、豊富なJavaエコシステムとシームレスに連携できる一方、Native(GraalVM Native Image)やWebブラウザ(Scala.js)でも実行可能です。さらに、型推論や簡潔な構文により、安全性を保ちながら高い開発生産性を実現します。
本書では、Scalaの基本的な構文や概念、そしてよく使われるライブラリやフレームワークについて学ぶことができます。具体的には、以下の内容が含まれます。
- Scalaの基本的な構文
- 変数や定数の定義
- 制御フロー(if文、for文、while文)
- 関数の定義と呼び出し
- オブジェクト指向プログラミング
- クラスとオブジェクトの定義
- 継承、ポリモーフィズム、カプセル化
- ケースクラスやトレイトの使い方
- 関数型プログラミング
- 関数型プログラミングの基本的な概念
- 高階関数、無名関数、クロージャーの定義と使い方
- パターンマッチングの使い方
- ライブラリやフレームワーク
- AkkaやSparkなどの分散処理ライブラリ
- Play FrameworkやAkka HTTPなどのWebアプリケーションフレームワーク
本書を読むことで、Scalaの基本的な構文やオブジェクト指向プログラミング、関数型プログラミングの概念を理解し、実際にライブラリやフレームワークを使ってアプリケーションを開発することができるようになるでしょう。また、Scalaの特徴や利点を理解し、Javaに比べた生産性の向上を体感することができます。
はじめに
[編集]Hello, World!
[編集]他の多くのチュートリアルがそうであるように、私たちもまずはScalaの世界にあいさつすることから始めましょう。 hello.scalaというファイルを作り(Scalaではソースファイルに.scalaという拡張子を付けることが通例となっています)、次のように書いて保存して下さい。
- hello.scala
-
- Scala 3
object Main extends App: println("Hello world!")
- Scala 2
object Main extends App { println("Hello world!") }
Scalaの基礎
[編集]変数と型システム
[編集]変数宣言
[編集]Scalaでは、変数は以下の2種類の方法で宣言できます:
キーワード | 意味 | 例 |
---|---|---|
val | 不変(イミュータブル) | val x: Int = 42
|
var | 可変(ミュータブル) | var y: String = "Hello"
|
- 型推論の例:
val number = // Int型と推論される val text = "Scala" // String型と推論される val list = List(1, 2) // List[Int]型と推論される
基本データ型
[編集]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.14159
|
Boolean | 真偽値 | val b: Boolean = true
|
Char | 16ビットUnicode文字 | val c: Char = 'A'
|
String | 文字列 | val s: String = "Hello"
|
モダンな制御構造
[編集]if式
[編集]Scalaのif式は値を返すことができます:
// Scala 3 val result = if x < 0 then "negative" else if x == then "zero" else "positive" // Scala 2 val result = { if (x < 0) { "negative" } else if (x == 0) { "zero" } else { "positive" } }
for内包表記
[編集]コレクションの操作に便利なfor式:
val numbers = List(1, 2, 3, 4, 5) // Scala 3 val squared = for x <- numbers if x % 2 == 0 yield x * x // Scala 2 val squared = for { x <- numbers if x % 2 == 0 } yield x * x
Scalaの制御構造とイテレーション処理について説明します。
- 基本的な制御構造:
// if 式 val x = 10 if x > 0 then println("positive") else if x < 0 then println("negative") else println("zero") // while ループ var i = 0 while i < 5 do println(i) i += 1 // match 式 val number = 3 number match case 1 => "one" case 2 => "two" case n => s"other: $n"
- コレクションのイテレーション:
// for 式 (for comprehension) val numbers = List(1, 2, 3, 4, 5) // 基本的なイテレーション for n <- numbers do println(n) // フィルター付きイテレーション for n <- numbers if n % 2 == 0 do println(n) // 複数のジェネレーター for i <- 1 to 3 j <- 1 to i do println(s"$i, $j") // for式での値の生成 val squares = for n <- numbers yield n * n // flatMap と map の組み合わせ numbers.map(_ * 2) numbers.filter(_ > 2) numbers.flatMap(n => List(n, n * 2))
- 高階関数を使用したイテレーション:
val list = List(1, 2, 3, 4, 5) // foreach list.foreach(println) // map val doubled = list.map(_ * 2) // filter val evens = list.filter(_ % 2 == 0) // fold val sum = list.fold(0)(_ + _) // reduce val product = list.reduce(_ * _) // zipWithIndex list.zipWithIndex.foreach((value, index) => println(s"Index: $index, Value: $value") )
- Range とシーケンス:
// Range の生成 val range1 = 1 to 5 // 1から5まで(5を含む) val range2 = 1 until 5 // 1から4まで(5を含まない) val range3 = 1 to 10 by 2 // 1, 3, 5, 7, 9 // イテレータの使用 val iterator = List(1, 2, 3).iterator while iterator.hasNext do println(iterator.next())
- パターンマッチングを使用したイテレーション:
// リストのパターンマッチング def processList(list: List[Int]): Unit = list match case Nil => println("Empty list") case head :: tail => println(s"Head: $head") processList(tail) // タプルのパターンマッチング val pairs = List((1, "one"), (2, "two")) for (number, word) <- pairs do println(s"$number is written as $word")
- コレクションの変換とフィルタリング:
val numbers = List(1, 2, 3, 4, 5) // collect: パターンマッチングと変換を組み合わせる val evenDoubled = numbers.collect { case n if n % 2 == 0 => n * 2 } // partition: 条件に基づいて2つのリストに分割 val (evens, odds) = numbers.partition(_ % 2 == 0) // groupBy: 要素をグループ化 val grouped = numbers.groupBy(_ % 2)
これらの制御構造とイテレーション手法を組み合わせることで、さまざまな処理を簡潔に記述できます。状況に応じて最適な方法を選択することが重要です:
- 単純な繰り返し → for式やforeachを使用
- 値の変換 → mapを使用
- フィルタリング → filterまたはfor式with guardを使用
- 複雑な処理 → 高階関数を組み合わせる
- パターンマッチが必要な場合 → match式やcollectを使用
また、パフォーマンスを考慮する場合は、view
やIterator
を使用して中間コレクションの生成を避けることもできます。
関数定義
[編集]基本的な関数定義
[編集]def add(x: Int, y: Int): Int = x + y // 型推論を使用した場合 def multiply(x: Int, y: Int) = x * y // 複数行の関数 // Scala 3 def factorial(n: Int): Int = if n <= 0 then 1 else n * factorial(n - 1) // Scala 2 def factorial(n: Int): Int = { if (n <= 0) 1 else n * factorial(n - 1) }
デフォルト引数と名前付き引数
[編集]def greet(name: String, greeting: String = "Hello"): String = s"$greeting, $name!" // 呼び出し方法 greet("World") // "Hello, World!" greet("Scala", "Welcome") // "Welcome, Scala!" greet(greeting = "Hi", name = "User") // "Hi, User!"
オブジェクト指向プログラミング
[編集]クラスとオブジェクト
[編集]クラス定義
[編集]// Scala 3 class Person(val name: String, var age: Int): def greet(): String = s"Hello, I'm $name" def birthday(): Unit = age += 1 // Scala 2 class Person(val name: String, var age: Int) { def greet(): String = s"Hello, I'm $name" def birthday(): Unit = age += 1 }
ケースクラス
[編集]case class Point(x: Int, y: Int): def move(dx: Int, dy: Int): Point = Point(x + dx, y + dy)
トレイト
[編集]trait Printable: def print(): Unit trait Loggable: def log(message: String): Unit = println(s"[LOG] $message") class Document extends Printable with Loggable: def print(): Unit = log("Printing document...") // 印刷の実装
関数型プログラミング
[編集]高階関数
[編集]// リストの変換 val numbers = List(1, 2, 3, 4, 5) val doubled = numbers.map(x => x * 2) val evenOnly = numbers.filter(x => x % 2 == 0) // 関数の合成 val addOne = (x: Int) => x + 1 val multiplyByTwo = (x: Int) => x * 2 val composed = addOne andThen multiplyByTwo
パターンマッチング
[編集]sealed trait Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape def area(shape: Shape): Double = shape match case Circle(r) => math.Pi * r * r case Rectangle(w, h) => w * h
モダンなエコシステム
[編集]ZIOによる効果型システム
[編集]import zio.* object MyApp extends ZIOAppDefault: def run = for _ <- Console.printLine("What is your name?") name <- Console.readLine _ <- Console.printLine(s"Hello, $name!") yield ()
http4sによるWebサービス
[編集]import cats.effect.IO import org.http4s.* import org.http4s.dsl.io.* object HelloWorld: val routes = HttpRoutes.of[IO] { case GET -> Root / "hello" / name => Ok(s"Hello, $name!") }
CirceによるJSON処理
[編集]import io.circe.*, io.circe.generic.auto.*, io.circe.parser.*, io.circe.syntax.* case class User(name: String, age: Int) val user = User("John", 30) val json = user.asJson.noSpaces val decoded = decode[User](json)
実践的なプログラミング例
[編集]オンラインショッピングシステムの例
[編集]case class Product(id: String, name: String, price: BigDecimal) case class CartItem(product: Product, quantity: Int) case class ShoppingCart(items: List[CartItem]): def total: BigDecimal = items.map(item => item.product.price * item.quantity).sum def addItem(product: Product, quantity: Int): ShoppingCart = val newItem = CartItem(product, quantity) ShoppingCart(items :+ newItem) def removeItem(productId: String): ShoppingCart = ShoppingCart(items.filterNot(_.product.id == productId))
コメント
[編集]Scalaには、一般的なプログラミング言語と同様に、コード内にコメントを挿入する機能があります。Scalaのコメントは、Javaと同様に、複数行コメントや単一行コメントの形式をサポートしています。以下に、Scalaでのコメントの例を示します。
// これは単一行コメントです。 /* これは 複数行コメントです。 */ /** * これはドキュメンテーションコメントです。 * ScalaDoc形式のコメントで、関数やクラスのドキュメントを生成するために使用されます。 */
また、ScalaではScalaDocというドキュメンテーションツールを使用して、ソースコード内のコメントからドキュメントを生成することができます。このようなコメントは、コードの可読性やメンテナンス性を向上させるために重要です。
ScalaDoc
[編集]ScalaDocは、Scalaのソースコード内に記述されたコメントから自動的にドキュメントを生成するツールです。JavaのJavadocに似ていますが、Scalaの特性に合わせて拡張されたものです。
ScalaDocは、特定の形式のコメントに基づいて、ソースコードのAPIドキュメントを生成します。主な特徴は次のとおりです:
- コメントの形式: ScalaDocは、コメントが
/** */
で囲まれていることを前提としています。これにより、複数行のコメントや特定のタグを含むことができます。 - タグ: ScalaDocコメントには、さまざまなタグを使用して特定の情報を指定できます。例えば、
@param
タグは関数のパラメータに関する情報を提供し、@return
タグは関数の戻り値に関する情報を提供します。 - HTML出力: ScalaDocは通常、HTML形式のドキュメントを生成します。これにより、ブラウザで簡単に参照できる形式でAPIドキュメントを提供できます。
ScalaDocを使用することで、ソースコードの理解や使用方法のドキュメント化が容易になります。特に大規模なプロジェクトやライブラリでは、正確で明確なドキュメントは非常に重要です。
以下は、ScalaDocの例です。これは、簡単なScalaのクラスとそのメソッドのドキュメントを示しています。
- Scala 3
/** A simple class representing a point in 2D space.
*
* @constructor Creates a new point with given coordinates
* @param x The x-coordinate of the point
* @param y The y-coordinate of the point
*/
class Point(val x: Double, val y: Double):
/** Calculates the distance between this point and another point.
*
* The distance is calculated using the Euclidean distance formula:
* ```
* distance = √((x₂-x₁)² + (y₂-y₁)²)
* ```
*
* @param other The other point to calculate the distance to
* @return The Euclidean distance between this point and the other point
*/
def distanceTo(other: Point): Double =
val dx = this.x - other.x
val dy = this.y - other.y
math.hypot(dx , dy)
/** Main entry point of the program.
*
* Demonstrates the usage of the [[Point]] class by creating two points
* and calculating the distance between them.
*/
@main def Main: Unit =
val p1 = Point(0, 0)
val p2 = Point(3, 4)
println(s"Distance between p1 and p2: ${p1.distanceTo(p2)}")
- Scala 2
: /** * This is a simple class representing a point in 2D space. * * @param x The x-coordinate of the point. * @param y The y-coordinate of the point. */ class Point(val x: Double, val y: Double) { /** * Calculates the distance between this point and another point. * * @param other The other point to calculate the distance to. * @return The distance between this point and the other point. */ def distanceTo(other: Point): Double = { val dx = this.x - other.x val dy = this.y - other.y math.hypot(dx , dy) } } /** * Main entry point of the program. */ object Main extends App { val p1 = new Point(0, 0) val p2 = new Point(3, 4) println(s"Distance between p1 and p2: ${p1.distanceTo(p2)}") }
この例では、Point
クラスとそのメソッドに対するScalaDocコメントが提供されています。これにより、コードを理解する人や、このクラスやメソッドを使用する人が、それらの動作や使い方について理解しやすくなります。また、ScalaDocコメントには@param
や@return
といったタグも使用されており、それぞれパラメータや戻り値に関する情報を提供しています。
複数行コメントのネスト
[編集]Scalaの複数行コメントはネストできます。つまり、複数行コメントの中にさらに別の複数行コメントを含めることができます。これにより、コード内で複数の段落や区切りのあるコメントを組み合わせることができます。
以下は、Scalaでの複数行コメントのネストの例です。
/* これは外側の複数行コメントです。 /* これは内側の複数行コメントです。 内側のコメントは外側のコメントの中にネストされています。 */ 外側のコメントは内側のコメントの外側にあります。 */
このように、Scalaでは複数行コメントを適切にネストすることができます。
; の自動挿入
[編集]Scalaでは、JavaScriptやGoの様に ; が自動挿入されます。
❌
var x = 1 + 2 + 3
⭕
var x = 1 + 2 + 3
変数と型とリテラル
[編集]Scalaは、関数型プログラミング言語ですが、純粋関数言語ではないので変数があります。
変数の宣言と初期化と参照
[編集]Scalaで変数を使うためには、宣言が必要です。
val:イミュータブル変数宣言
[編集]- コード例
// val i: Int = 0 // !!! error: expected class or object definition !!! object Main extends App { val i: Int = 123 // i = 0 // !!! error: reassignment to val !!! println(i) }
- 実行結果
123
- 1行目は、Object の定義の外で宣言しようとしても失敗する例です。
- Scala には、グローバル変数はありません。
- // から行末まではコメントになります。
- object Main の中で、Int型の変数 i を宣言し、123 で初期化しています。
- val で宣言した i に 0 を代入しようとしていますがエラーになります。
- val で宣言した変数に、代入する事は出来ません。この様な変数をイミュータブルな変数と言います。
println(i)
の様に、変数の値は変数名(この場合は i)を使って参照できます。
var:ミュータブル変数宣言
[編集]- コード例
// var i: Int = 0 // !!! error: expected class or object definition !!! object Main extends App { var i: Int = 123 i = 0 println(i) }
- 実行結果
0
- 1行目は、Object の定義の外で宣言しようとしても失敗する例です。
- ミュータブルであっても、グローバル変数は作れません。
- object Main の中で、Int型の変数 i を宣言し、123 で初期化しています。
- var で宣言した i には 0 を代入する事ができます。この様な変数をミュータブルな変数と言います。
変数宣言に関する詳細
[編集]未初期化はコンパイルエラー
[編集]- コード例(コンパイルエラー)
object Main extends App { var i: Int println(i) }
- コンパイル結果
1 error Main.scala:2: error: only traits and abstract classes can have declared but undefined members (Note that variables need to be initialized to be defined) var i: Int ^
_ を使ったディフォルト値を使った初期化
[編集]- コード例
object Main extends App { var i: Int = _ println(i) var f: Boolean = _ println(f) var s: String = _ println(s) var m: Map[String, Int] = _ println(m) }
- 実行結果
0 false null null
- 識別子
_
を初期化に使うと、その型のディフォルト値で初期化されます。- 数値は 0、Boolean は false、他は null がディフォルト値となります。
- Go のゼロ値と似ていますが、明示的に _ で初期化するところが違います。
同じ式による複数の変数の初期化
[編集]- コード例
object Main extends App { val i, j, k = 42 println(i, j, k) val u, v, w = new Array(5) println(u, v, w) var n: Int = 0 val x, y, z = { n += 1 ; n } println(x, y, z) }
- 実行結果
(42,42,42) ([Ljava.lang.Object;@7c16905e,[Ljava.lang.Object;@2a2d45ba,[Ljava.lang.Object;@2a5ca609) (1,2,3)
- 複数の変数を、一度に宣言し同じ式[1]で初期化することが出来ます。
val i, j, k = 42
で k だけではなく j も i も初期化されます。- 初期化の式は、リテラルである必要はなく、初期化される宣言毎に評価されます。
val u, v, w = new Array(5)
のnew Array(5)
は、3回評価され、その都度新しいインスタンスが生成されます。val x, y, z = { n += 1 ; n }
の{ n += 1 ; n }
は、3回評価され、その都度インクリメントされるので、C言語の列挙型 enum と同じ機能が実現できます。どちらかと言うと Goのiota
ですね。{ n += 1 }
ではなく{ n += 1 ; n }
なのは、n += 1
の値は n ではなく()
だからです。- このため、Scala では
x = y = 42
とは出来ず(x に () が入ってしまう)、(x, y) = (42, 42)
のようにします。
パターンマッチ
[編集]変数宣言にもパターンマッチングが使えます。
val (x, y) = ( 178, 901 ) val v @ (a, a) = ( 42, 78 ) val m @ Array(n0, n1, _*) = Array(2,3,5,7,11)
lazy val:遅延評価
[編集]- コード例
object Main extends App { var a = 3 var b = 4 lazy val c = { println("!"); a * b } a = 0 println(c) a = 100 println(c) }
- 実行結果
! 0 0
lazy val
で宣言された変数は、はじめて参照された6行目で評価- この時に参照されるのは、5行目で更新された a の値が参照されるので、クロージャではありません。
- 8行目で再び、c を参照すると、7行目での a の変更は反映されません。一度、評価されたら値は変わりません。
lazy var は出来ません。
言い換えると、「変数をイミュータブルにすべきか?」と言う質問になります。
関数プログラミングの観点からは、そもそも変数(=副作用)は邪道なのですが、 あえて変数を使うのであれば、まず val(イミュータブル)で宣言し、代入が行われるようなら var(ミュータブル)に変更するという基本戦略が考えられます。
幸いScalaは、イミュータブルな変数とミュータブルな変数に使える名前(変数名)の規則は同じで、参照するときも追加の記号は必要ありません。型推論
[編集]先の例では、変数を型を指定して宣言していました。 Scalaには、型推論機構があるので、初期値が与えられる変数宣言では初期値から変数の型が推論され型名を明示する必要はなくなります。
- 型推論
object Main extends App { val i = 123 var j = 12 println(i) println(j) }
- 実行結果
123 12
数値のリテラル表現
[編集]Javaの数値はオブジェクトではなくプリミティブ型ですが、Scalaでは数値もオブジェクトです。 また型名にも違いがあるので、対照表にしてみました。
JavaとScaleの型の対照表 Javaでの型 Scalaでの型 サイズ リテラル表現 char Char 2バイト 'A', '漢' byte Byte 1バイト -128:Byte short Short 2バイト 123:Short int Int 4バイト 1234567890, -2147483648 long Long 8バイト 123L, -9223372036854775808L float Float 4バイト 3.1415926536f double Double 8バイト 3.1415926536 boolean Boolean 1バイト true, false
ブロック
[編集]Scalaでは、カーリーブラケット構文を使ってブロックを表します。 ブロックの最後の式の値が、ブロックの値になります。
- ブロックとブロックの値
object Main extends { val x = { val (a, b) = (1, 2) println("Hello!") a + b } println(x) }
- 実行結果
Hello! 3
val (a, b) = (1, 2)
は、タプルを使った変数の宣言です。- これらの変数のスコープはブロック末の '}' までで、それ以降は参照できません(ブロックスコープ)。
関数
[編集]Scalaでは、「0個以上のパラメータを受取ることの出来る式」を関数と呼びます。 他の言語では、アロー関数やラムダ式と呼ばれる概念です。
- 関数
object Main extends App { val add = (x: Int, y: Int) => x + y val msg = () => "hello!" println(add(1, 2)) println(add(1999, 1)) println(msg()) println(((a: Int,b: Int)=>a*b)(9, 11)) }
- 実行結果
3 2000 hello! 99
- 関数には名前がないので、変数に一旦に束縛し変数(変数名)を使って呼出します。
- 8行目では、変数を介さず定義したその場で実行しています(無名関数の即時実行)。
無名関数のアンダースコアを使った簡略表記
[編集]- コード例
object Main extends App { println((0 to 4).map(x => x * 3)) println((0 to 4).map(_ * 3)) println((0 to 4).reduce((x, y) => x + y)) println((0 to 4).reduce(_ + _)) println((0 to 4).map(x => x * x)) // 参照が二回以上ある場合は明示的に名前をつける }
- 実行結果
Vector(0, 3, 6, 9, 12) Vector(0, 3, 6, 9, 12) 10 10 Vector(0, 1, 4, 9, 16)
パイプラインスタイル
[編集]パイプラインスタイルは、JavaScriptなどでよく使うメソッドチェインですが、イテレータメソッドが演算子の様に見える書き方です。
for と Range と ifの例をパイプラインスタイルで置換えてみます。
- 例
object Main extends App { // for と range と if for (i <- 0 to 10 if i % 2 != 0) println(i) // Scala 3 // for i <- 0 to 10 if i % 2 != 0 do println(i) (0 to 10).filter(_ % 2 != 0).foreach(println(_)) // よりScala 3らしい for 式の書き方 for i <- 0 to 10 if i % 2 != 0 do println(i) // Scala 2 (0 to 10) filter { _ % 2 != 0 } foreach { println(_) } }
- 実行結果
1 3 5 7 9
関数合成ではないので、(0 to 10) filter { _ % 2 != 0 }
が先に評価され、そのオブジェクトに .foreach({ println(_) })
が提供されるので、場合によっては要素数の多いコレクション・オブジェクトが中間結果として生成されるので注意してください。
メソッド
[編集]メソッドは、コードを再利用する仕組みで、関数と似ていますが、両者は別個のものです。 メソッドはclass、object、trait内でキーワードdefを使って定義します。
- メソッド
object Main extends App { def add(x: Int, y: Int): Int = x + y def msg: String = "hello!" println(add(1, 2)) println(add(1999, 1)) println(msg) }
- 実行結果
3 2000 hello!
- これだけ観ると、構文はともかく関数とメソッドの差は大きく感じられません。
- 強いていうと、空の引数リストは定義も呼出しも () を付けないところが違います。
これまで、頻繁に使ってきた println()
も、Predefオブジェクトのメソッドで、Predefオブジェクトは暗黙に import されるので特別な準備なく使えました。
遅延評価パラメータ
[編集]Scalaでは、メソッドや関数の引数は値渡しですが、遅延評価にすることが出来ます。 これによって、制御構造を関数定義する道が開きます。
- メソッド
object Main extends App { def f1(x: Boolean, t: Unit, f: Unit) = if (x) t else f f1(true, println("True"), println("False")) def f2(x: Boolean, t: => Unit, f: => Unit) = if (x) t else f f2(true, println("真"), println("偽")) }
- 実行結果
True False 真
メインメソッド
[編集]メインメソッドは、プログラムのエントリーポイントです。
制御構造
[編集]Scalaも、多くのプログラミング言語と同じく、「逐次」「分岐」「反復」の 3 種類の制御構造を持ちます。 Scalaの制御構造は、全て値を持ちます。この事は、Scalaの If式 は、C言語の三項演算子と等価であることを示します。
Scala 3 の新構文
[編集]Scala 3 では、括弧()
を使わない新しい構文が追加されました。
新構文の登場で、旧構文が使えなくなることはないので、Scala 2の為に書かれたプログラムが使えなくなるような事はありません[2]。
Scala 2 | Scala 3 |
---|---|
object Main extends App {
val x = 0.0 / 0.0
val xs = 0 to 4
val ys = 0 to 5
// 条件分岐
val result = {
if (x < 0) {
"負"
} else if (x == 0) {
"零"
} else if (x > 0) {
"正"
} else {
"何"
}
}
println(result)
// 条件式を用いた絶対値計算
val absX = if (x < 0) -x else x
println(s"絶対値: $absX")
// whileループ
var counter = x
while (counter >= 0) {
print(s"$counter ")
counter -= 1
}
println()
// for式(条件付きループ内包表記)
val squares = for {
x <- xs
if x > 0
} yield x * x
println(s"平方数: $squares")
// ネストしたfor式
for {
x <- xs
y <- ys
} {
println(s"$x + $y = ${x + y}")
}
}
|
object Main extends App:
val x = 0.0 / 0.0
val xs = 0 to 4
val ys = 0 to 5
// 条件分岐
val result =
if x < 0 then "負"
else if x == 0 then "零"
else if x > 0 then "正"
else "何"
println(result)
// 条件式を用いた絶対値計算
val absX = if x < 0 then -x else x
println(s"絶対値: $absX")
// whileループ
var counter = x
while counter >= 0 do
print(s"$counter ")
counter -= 1
println()
// for式(条件付きループ内包表記)
val squares = for x <- xs if x > 0 yield x * x
println(s"平方数: $squares")
// ネストしたfor式
for
x <- xs
y <- ys
do println(s"$x + $y = ${x + y}")
|
Scala 3の新構文の特徴とそのモチベーションについて解説します:
主な変更点と導入の理由は以下の通りです:
- インデントベースの構文
- 特徴:
- 波括弧
{}
の代わりにインデントでブロックを表現 - コロン
:
とインデントで新しいブロックを開始
- 波括弧
- モチベーション:
- コードの視覚的な明確さの向上
- ネストされた構造の把握が容易に
- Python等の人気言語との親和性
- ボイラープレートコードの削減
- 特徴:
then
キーワードの導入- 特徴:
if
条件式の後にthen
を使用- 括弧
()
が不要に
- モチベーション:
- 条件部分と実行部分の区別を明確に
- 読みやすさの向上
- 構文の一貫性の確保
- 特徴:
- 制御構造の簡素化
- 特徴:
while
、for
などでdo
を使用- ブロックの開始がより明確に
- モチベーション:
- 制御構造の統一感
- コードの意図の明確化
- 学習曲線の緩和
- 特徴:
- オプショナルな括弧
- 特徴:
- 多くの場面で括弧が省略可能に
- メソッド呼び出しや条件式で特に顕著
- モチベーション:
- 余分な記号の削減
- コードの簡潔性の向上
- 関数型プログラミングスタイルの促進
- 特徴:
- トップレベル定義
- 特徴:
- パッケージオブジェクトなしでトップレベルに定義可能
- より直感的なコード構造
- モチベーション:
- ボイラープレートの削減
- コード組織化の簡素化
- モジュール性の向上
- 特徴:
given
とusing
の導入(implicit の代替)- 特徴:
- より明示的な依存性注入の構文
- コンテキストの受け渡しがより明確に
- モチベーション:
- 暗黙の振る舞いの明確化
- コードの追跡可能性の向上
- 型システムの強化
- 特徴:
- Enums とADTsの改善
- 特徴:
- より簡潔な列挙型の定義
- パターンマッチングの改善
- モチベーション:
- 代数的データ型の扱いの改善
- よりエレガントなドメインモデリング
- 型安全性の向上
- 特徴:
- 拡張メソッド(Extension Methods)
- 特徴:
- より明示的な拡張メソッドの定義
- 型クラスパターンの簡素化
- モチベーション:
- コードの再利用性の向上
- 暗黙の変換に頼らない拡張機能
- より安全な型の拡張
- 特徴:
これらの変更は、以下の全体的な目標に基づいています:
- 可読性の向上:より直感的で理解しやすいコード
- 安全性の強化:型システムの改善と暗黙の動作の削減
- 学習の容易さ:より一貫性のある、学びやすい言語仕様
- 生産性の向上:ボイラープレートの削減と表現力の向上
- モダン化:現代的なプログラミング手法との調和
この新構文により、Scalaはより表現力豊かで、安全で、使いやすい言語となっています。
分岐
[編集]if
[編集]if式は、条件式に基づいて分岐し分岐先の式を評価します。 if式の値は、分岐先の式の値です。
- 構文
if-expr := if '(' 条件式 ')' 式1 [ else 式2 ]
- 条件式は、Boolean 型でなければいけません。
else 式2
の部分はオプショナルで、省略され条件式が false であった場合、if式はUnit 型の値()
単位ユニットが式の値となります(要素個数ゼロのタプルです)。- elseif elsif のたぐいはないので、
else if (条件式2) 式3
と続けます。 - if式の例
object Main extends App { val i = 0 if (i == 0) println("zero") else println("non zero") println({ if (i == 0) "Zero" else "Non zero" }) println({ if (i != 0) "NON ZERO" }) }
- 実行結果
zero Zero ()
match
[編集]C言語のswicth文は、式と一致するリテラルのラベルへの多方向GOTO文ですが、Scalaのmatch式はパターンマッチングに基づいています。
- 構文
match-expr := 式 match ’{’ ( パターン => 式 )+ ’}’
- 式の値に、合致するパターンはないか上から順に走査し、合致したパターン対応する式が評価され、この値がmatch式の値となります。
- 式に対して、パターンがどれも合致しないとコンパイル時のフロー解析の結果わかった場合、コンパイルエラーになります。
- このため、全てにマッチするパターン _ が最後に置かれることが常となります。
- ただし式が、Boolean や enum の場合は、網羅チェックを台無しにするので _ を使うべきではありません。
- match式の例
object Main extends App { val n = 10 n match { case 1 => println("壱") case 2 => println("弐") case 3 => println("参") case _ => println("多数") } println({ n match { case 1 => "一" case 2 => "二" case 3 => "三" case _ => "膨大" } }) println({ n match { case 1 => "一" case 2 => "二" case 3 => "三" // case _ => "膨大" // ここをコメントにしてしまうと、網羅性がほころびコンパイルエラーになる } }) }
- 実行結果
多数 膨大 膨大
反復
[編集]while
[編集]while式は、条件式が true の間、式を評価しつづけます。 while式の値は、評価した式の値です。
- 構文
while-expr := while '(' 条件式 ')' 式
- 条件式は、Boolean 型でなければいけません。
- while式の例
object Main extends App { var i = 0 while (i < 5) { println(i) i += 1 } println(s"last = $i") }
- 実行結果
1 2 3 4 last = 5
- Scalaには、
変数 ++
の様なインクリメント演算子はなく、変数 += 1
の構文を使います。- これは、Scala の整数はイミュータブルなためで、同じ理由で Ruby にもインクリメント演算子はありません。
- また、
変数 += 1
は、変数 = 変数 + 1
の構文糖です。 - さらに、
変数 + 1
は、変数 .+(1)
の構文糖です。- 加算に限らず、Scalaの演算子は、メソッドの構文糖です。
- i のスコープは、object Main なのでループを抜けても値を参照出来ます。
s"last = $i"
は、加工文字列リテラル(processed string literal) で、文字列の中で変数の値を参照できます。
do - while
[編集]Scala 3 で、do - while は廃止になりました。 while の条件式に繰返し部分と続行条件を書き、ループ本体は空にすることで do - while を模倣できます。
- do - while の模倣
object Main extends App { var i = 10 while ({ println(i) i += 1 i < 5 }) {} println(s"last = $i") }
- 実行結果
10 last = 11
for
[編集]Scalaのfor式は、ジェネレータに対してイテレーションを行います。
- 構文
for-expr := for '(' ジェネレータ ')' 式
for と Range
[編集]- for式とRangeクラスの例
// Scala 3 object Main extends App: println(0 to 4) for i <- 0 to 4 do println(s"to: $i") println(0 until 4) for i <- 0 until 4 do println(s"until: $i") // Scala 2 object Main extends App { println(0 to 4) for (i <- 0 to 4) println("to: " + i) println(0 until 4) for (i <- 0 until 4) println("until: " + i) }
- 実行結果
Range 0 to 4 to: 0 to: 1 to: 2 to: 3 to: 4 Range 0 until 4 until: 0 until: 1 until: 2 until: 3 class scala.collection.immutable.Range$Inclusive
0 to 4
や0 until 4
は、Rangeクラスのオブジェクトで範囲を表します。0 to 4
や0 until 4
は、Rangeクラスのリテラル、、ではなくto
とuntil
は演算子で0.to(4)
や0.until(4)
と言うメソッドの演算子表記です。
for と Range と if
[編集]ジェネレータに、if で修飾することができます(Guardといいます)。Pythonのジェネレータ式に似た表現です。
- for式とRangeクラスとif式の例
object Main extends App { for (i <- 0 to 10 if i % 2 != 0) println(i) }
- 実行結果
1 3 5 7 9
二重ループ
[編集]2つのジェネレーターを ;
で区切ると多重ループを構成出来ます。
- 二重ループ
object Main extends App { for ( i <- 0 to 2; j <- 0 to 3 ) { println((i, j)) } }
- 実行結果
(0,0) (0,1) (0,2) (0,3) (1,0) (1,1) (1,2) (1,3) (2,0) (2,1) (2,2) (2,3)
値を返す for
[編集]for式と yeild
の組合せでfor式は値を返すことが出来ます。
- 値を返す for
object Main extends App { println(for (i <- 0 to 4) yield i.toString) }
- 実行結果
Vector(0, 1, 2, 3, 4)
脱出
[編集]return
[編集]ブロックからの脱出には、return を使います。 break や continue はありません。
例外処理
[編集]ScalaにはJavaのthrows節がありません[3]。 すべての例外はメソッドの外側に投げることができます。
- 例外処理
object Main extends App { try { val n = 1 n / 0 println("return!") } catch { case e: Exception => { println("Exception caught.") println(e.getMessage) println(e.getStackTrace) } } finally { println("done!") } }
- 実行結果
Exception caught. / by zero [Ljava.lang.StackTraceElement;@35a74ae9 done!
- finally 節の式は、例外の有無に関係なく実行されます。
- try 節や catch 節 で exit() すると、流石に finally 節は実行されません。
クラス
[編集]Scalaは、関数型プログラミング言語であると同時に、オブジェクト指向プログラミング言語です。 より厳密に言うと、(プロトタイプベースではなく)クラスベースのオブジェクト指向プログラミング言語です。 クラス(class)は、オブジェクトを作る雛形で、クラスから new 演算子を使ってオブジェクトを作ることをインスタンス化、出来たオブジェクトの事をインスタンスと呼びます。
クラス定義とインスタンス化とメソッド
[編集]- コード例
object Main extends App { class Hello(val s: String = "world") { override def toString: String = s"Hello $s!" def print: Unit = println(s) } val hello1 = new Hello() println(hello1) hello1.print val hello2 = new Hello("my friend") println(hello2) print(s""" Hello.getClass() === ${Hello.getClass()} hello1 === ${hello1} hello2.s = ${hello2.s} """) }
- 実行結果
Hello world! world Hello my friend! Hello.getClass() === class Main$Hello$ hello1 === Hello world! hello2.s = my friend
- Ruby#クラスの例を、Scala に移植しました。
- 冒頭4行がクラス定義です。
- クラス定義に、他のオブジェクト指向言語ならコンストラクタに渡すような引数が渡されています。
- メンバーを公開するケースなら、この様に宣言的な引数リストを使うとメンバー定義と暗黙の初期値を与えられます。
- toString は、オブジェクトを文字列化するメソッドで、Objectの同名のメソッドをオーバーライドしています。
- print は、このクラスに独自なメソッドで、println() の値 == () == Unit を戻値型としています。
少しまとまったサイズのクラス
[編集]Ruby#ユーザー定義クラスの都市間の大圏距離を求めるメソッドを追加した例を、Scalaに移植。
- ユーザー定義クラス
object Main extends App { class GeoCoord(val longitude: Double = 0.0, val latitude: Double = 0.0) { override def toString: String = { var (ew, ns) = ("東経", "北緯") var (long, lat) = (longitude, latitude) if (long < 0.0) { ew = "西経" long = -long } if (lat < 0.0) { ns = "南緯" lat = -lat } s"(${ew}: ${long}, ${ns}: ${lat})" } def distance(other: GeoCoord): Double = { val i = Math.PI / 180 val r = 6371.008 Math.acos( Math.sin(this.latitude * i) * Math.sin(other.latitude * i) + Math.cos(this.latitude * i) * Math.cos(other.latitude * i) * Math.cos(this.longitude * i - other.longitude * i) ) * r } } val Sites = Map( "東京駅" -> new GeoCoord(139.7673068, 35.6809591), "シドニー・オペラハウス" -> new GeoCoord(151.215278, -33.856778), "グリニッジ天文台" -> new GeoCoord(-0.0014, 51.4778) ) Sites.foreach { case (k, v) => println(s"${k}: ${v}") } val keys = Sites.map { case (k, v) => k }.toList val len = keys.size for (i <- 0 until len) { val (ksi, ksx) = (keys(i), keys((i + 1) % len)) println(s"${ksi} - ${ksx}: ${Sites(ksi).distance(Sites(ksx))} [km]") } }
- 実行結果
東京駅: (東経: 139.7673068, 北緯: 35.6809591) シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778) グリニッジ天文台: (西経: 0.0014, 北緯: 51.4778) 東京駅 - シドニー・オペラハウス: 7823.269299386704 [km] シドニー・オペラハウス - グリニッジ天文台: 16987.2708377249 [km] グリニッジ天文台 - 東京駅: 9560.546566490015 [km]
ケースクラス
[編集]- コード例
object Main extends App { case class GeoCoord(longitude: Double = 0.0, latitude: Double = 0.0) { override def toString: String = { var (ew, ns) = ("東経", "北緯") var (long, lat) = (longitude, latitude) if (long < 0.0) { ew = "西経" long = -long } if (lat < 0.0) { ns = "南緯" lat = -lat } s"(${ew}: ${long}, ${ns}: ${lat})" } def distance(other: GeoCoord): Double = { val i = Math.PI / 180 val r = 6371.008 Math.acos( Math.sin(this.latitude * i) * Math.sin(other.latitude * i) + Math.cos(this.latitude * i) * Math.cos(other.latitude * i) * Math.cos(this.longitude * i - other.longitude * i) ) * r } } val Sites = Map( "東京駅" -> GeoCoord(139.7673068, 35.6809591), "シドニー・オペラハウス" -> GeoCoord(151.215278, -33.856778), "グリニッジ天文台" -> GeoCoord(-0.0014, 51.4778) ) Sites.foreach { case (k, v) => println(s"${k}: ${v}") } val keys = Sites.map { case (k, v) => k }.toList val len = keys.size for (i <- 0 until len) { val (ksi, ksx) = (keys(i), keys((i + 1) % len)) println(s"${ksi} - ${ksx}: ${Sites(ksi).distance(Sites(ksx))} [km]") } }
- 実行結果
東京駅: (東経: 139.7673068, 北緯: 35.6809591) シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778) グリニッジ天文台: (西経: 0.0014, 北緯: 51.4778) 東京駅 - シドニー・オペラハウス: 7823.269299386704 [km] シドニー・オペラハウス - グリニッジ天文台: 16987.2708377249 [km] グリニッジ天文台 - 東京駅: 9560.546566490015 [km]
- コンストラクタ引数に val がなくても、自動的にフィールドが宣言されます。
- var なフィールドを希望する場合は、var を明示します。
- インスタンス化する時、new は必要ありません。
- コンパニオンオブジェクトが自動的に生えてきます。
シングルトン・オブジェクト
[編集]object は、たった1つのインスタンスを持つクラス(=シングルトン)です。 これは、lazy valのように、参照されたときに(遅延して)生成されます。
object は、トップレベルの値としてはシングルトンであり、 クラスのメンバやローカルな値としては、lazy val と全く同じ振る舞いをします。
シングルトン・オブジェクトの定義
[編集]シングルトン・オブジェクトは値です。シングルトン・オブジェクトの定義はクラスのように見えますが、object というキーワードを使用します。
シンプルなシングルトン
object Simple
トレイト
[編集]トレイト(trait)は、Javaのインターフェースと抽象クラスの中間的な存在です。 実装を持ったインターフェースで、コンストラクタのパラメータを持つことができませんが、ミックスイン( mix-in )することが出来ます。
- 抽象クラスと同様に、トレイトは具象メンバを持つことができますが、インスタンス化することはできません。
- インターフェイスと同様に、クラスは複数のトレイトのメンバを継承することができます。
- クラスはトレイトで拡張することができます。
class C extends Trait1 { ... }
- クラスや他の特徴を継承することができます。
trait T1 extends Class1 { ... } trait T2 extends Trait1 { ... }
- クラス、オブジェクト、トレイトはトレイトの中で混在することができます。
class C extends Trait1 with Trait2 with Trait3 { ... } val obj = new Class1() with Trait1 with Trait2 with Trait3 { ... }
改廃された技術
[編集]Scalaの改廃された技術や利用が推奨されない技術は、言語の進化、新しい要求、パフォーマンスの改善などによって置き換えられます。以下に、代表的な技術を示します。
Actor (scala.actors)
[編集]- サポート開始年: 2006年
- サポート終了年: 2013年(Scala 2.11で廃止)
- 廃止または衰退の理由
- 古いActorシステムは、パフォーマンスと機能の制限により、Akka Actorシステムに置き換えられました。
- 代替技術
- Akka Actors
- ZIO
- Cats Effect
XML リテラル
[編集]- サポート開始年: 2004年
- サポート終了年: 2019年(Scala 2.13で廃止)
- 廃止または衰退の理由
- 言語コア機能からXMLサポートを分離し、より柔軟な外部ライブラリとして提供することになりました。
- 代替技術
-
- scala-xml ライブラリ
- ScalaTagsなどのXML/HTMLテンプレートエンジン
Symbol リテラル
[編集]- サポート開始年: 2004年
- サポート終了年: 2019年(Scala 2.13で非推奨化)
- 廃止または衰退の理由
- 文字列との重複した機能性があり、パフォーマンス上のメリットが少ないため。
- 代替技術
-
- 通常の文字列
- 列挙型(Enumeration)
DelayedInit
[編集]- サポート開始年: 2008年
- サポート終了年: 2021年(Scala 3で廃止)
- 廃止または衰退の理由
- 初期化の遅延に関する複雑な問題と予期せぬ動作を引き起こす可能性があったため。
- 代替技術
-
- lazy val
- デフォルトパラメータ
- case classのコンパニオンオブジェクト
手続き型構文
[編集]- サポート開始年: 2004年
- サポート終了年: 2021年(Scala 3で大幅に簡素化)
- 廃止または衰退の理由
- より関数型プログラミングを促進し、コードの一貫性を高めるため。
- 代替技術
-
- 関数型構文
- forやwhileの代わりにmap/flatMap/filter
- コレクションのメソッドチェーン
Scala.swing
[編集]- サポート開始年: 2008年
- サポート終了年: アクティブな開発は終了
- 廃止または衰退の理由
- WebベースのUIやモバイルアプリケーションの台頭により、デスクトップGUIの需要が減少。
- 代替技術
-
- ScalaFX
- Web フレームワーク(Play Framework, http4s)
- クロスプラットフォームフレームワーク(Scala.js)
Package オブジェクト
[編集]- サポート開始年: 2008年
- サポート終了年: 2021年(Scala 3で非推奨化)
- 廃止または衰退の理由
- Scala 3では、より簡潔で理解しやすい代替手段が導入されました。
- 代替技術
-
- トップレベル定義
- 拡張メソッド
- ギブン・インスタンス
暗黙の変換(implicit conversion)
[編集]- サポート開始年: 2004年
- サポート終了年: 2021年(Scala 3で制限付き)
- 廃止または衰退の理由
- 暗黙の変換は予期せぬ動作やコードの読みづらさを引き起こす可能性があったため。
- 代替技術
-
- 明示的な拡張メソッド
- 型クラス
- given/using構文(Scala 3)
脚註
[編集]- ^ 値ではなく式です。
- ^ 2から3へのアップデートと言うと、python2とpython3の非互換性の悲劇を思い起こす方も多いと思いますが、Scaleでは原則的に下位互換性があります。
- ^ Javaの場合は、例外を発生させる可能性のあるメソッドは、throws節で発生させる例外の種類をすべて宣言する必要があります。 ただし、RuntimeException(RuntimeExceptionの派生クラス)の場合は、throws節で明示的に記述する必要はありません。
参考文献
[編集]- “Scala Language Specification | Scala 2.13”. École Polytechnique Fédérale, Lausanne (EPFL) Lausanne, Switzerland (2022年6月10日). 2022年6月24日閲覧。
- “Scala 3 Reference”. École Polytechnique Fédérale, Lausanne (EPFL) Lausanne, Switzerland (2022年6月1日). 2022年6月24日閲覧。