Scalaの紹介
1.はじめに
このチュートリアルでは、https://www.scala-lang.org/[Scala] – Java仮想マシンで動作する主要言語の1つ – を見ていきます。
値、変数、メソッド、制御構造などの主要な言語機能から始めます。次に、高階関数、カレー、クラス、オブジェクト、パターンマッチングなどの高度な機能について説明します。
JVM言語の概要については、https://www.baeldung.com/jvm-languages[JVM言語のクイックガイド]を参照してください。
2.プロジェクトの設定
このチュートリアルでは、https://www.scala-lang.org/download/から標準のScalaインストールを使用します。
まず、私たちのpom.xmlにscala-library依存関係を追加しましょう。この成果物は、言語の標準ライブラリーを提供します。
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.7</version>
</dependency>
次に、コードのコンパイル、テスト、実行、文書化のためにhttp://davidb.github.io/scala-maven-plugin/index.html[scala-maven-plugin]を追加しましょう。
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.3.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
Mavenにはhttps://mvnrepository.com/artifact/org.scala-lang/scala-library[scala-lang]およびhttps://mvnrepository.com/artifact/net.alchim31.maven/scala-mavenの最新の成果物があります-plugin[scala-maven-plugin]。
最後に、ユニットテストにJUnitを使用します。
3.基本機能
このセクションでは、例を通して基本的な言語機能を調べます。この目的のためにhttps://docs.scala-lang.org/overviews/repl/overview.html[Scalaインタプリタ]を使用します。
3.1. 通訳
インタプリタはプログラムや式を書くための対話型シェルです。
それを使って「こんにちは世界」を印刷しましょう。
C:\>scala
Welcome to Scala 2.12.6 (Java HotSpot(TM)
64-Bit Server VM, Java 1.8.0__92).
Type in expressions for evaluation.
Or try :help.
scala> print("Hello World!")
Hello World!
scala>
上記では、コマンドラインで「scala」と入力してインタプリタを起動しています。
インタプリタが起動し、ウェルカムメッセージとそれに続くプロンプトが表示されます。
次に、このプロンプトで式を入力します。インタプリタは式を読み、それを評価して結果を表示します。その後、ループしてプロンプトを再表示します。
それは即座のフィードバックを提供するので、インタプリタは言語を始めるための最も簡単な方法です。そのため、それを使って、基本的な言語機能、つまり式とさまざまな定義を探ります。
3.2. 式
-
計算可能ステートメントはすべて式** です。
式をいくつか書いてその結果を見てみましょう。
scala> 123 + 321
res0: Int = 444
scala> 7 ** 6
res1: Int = 42
scala> "Hello, " + "World"
res2: String = Hello, World
scala> "zipZAP" ** 3
res3: String = zipZAPzipZAPzipZAP
scala> if (11 % 2 == 0) "even" else "odd"
res4: String = odd
上記のように、** すべての式は値と型を持ちます。
-
式に返すものがない場合は、
Unit
** 型の値が返されます。このタイプは1つの値だけを持ちます:
()
。これはJavaの
void
キーワードに似ています。
3.3. 値の定義
キーワード
val
は値を宣言するために使用されます。
これを使って式の結果を命名します。
scala> val pi:Double = 3.14
pi: Double = 3.14
scala> print(pi)
3.14
そうすることで、結果を複数回再利用することができます。
-
値は不変です** 。したがって、それらを再割り当てすることはできません。
scala> pi = 3.1415
<console>:12: error: reassignment to val
pi = 3.1415
^
3.4. 変数定義
値を再割り当てする必要がある場合は、代わりにそれを変数として宣言します。
キーワード
__var
__は、変数を宣言するために使用されます。
scala> var radius:Int=3
radius: Int = 3
3.5. メソッド定義
def
キーワードを使ってメソッドを定義します。キーワードに続いて、メソッド名、パラメータ宣言、セパレータ(コロン)、および戻り型を指定します。その後、セパレータ(=)とそれに続くメソッド本体を指定します。
-
Javaとは対照的に、結果を返すために
return
キーワードを使用しません。メソッドは最後に評価された式の値を返します。
2つの数の平均を計算するためのメソッド
avg
を書きましょう。
scala> def avg(x:Double, y:Double):Double = {
(x + y)/2
}
avg: (x: Double, y: Double)Double
それでは、このメソッドを呼び出しましょう。
scala> avg(10,20)
res0: Double = 12.5
-
メソッドがパラメータをとらない場合は、定義および呼び出し中に括弧を省略することができます。さらに、本体に1つの式しかない場合は、中括弧を省略することができます。
無作為に「Head」または「Tail」を返すパラメータなしのメソッド
__coinToss
__を書きましょう。
scala> def coinToss = if (Math.random > 0.5) "Head" else "Tail"
coinToss: String
次に、このメソッドを呼び出しましょう。
scala> println(coinToss)
Tail
scala> println(coinToss)
Head
4.制御構造
制御構造によって、プログラム内の制御の流れを変えることができます。
以下の制御構造があります。
-
If-else式
-
whileループとwhileループ
-
表現用
-
表現してみてください
-
一致表現
Javaとは異なり、
continue
や
break
というキーワードはありません。
return
キーワードがあります。しかし、私たちはそれを使わないでください。
switch
ステートメントの代わりに、一致表現によるパターンマッチングがあります。さらに、独自のコントロール抽象化を定義できます。
4.1. if-else
__if-else
式はJavaに似ています。
else__部分はオプションです。複数のif-else式を入れ子にすることができます。
-
式なので値** を返します。そのため、Javaの3項演算子(?:)と同じように使用します。実際、この言語には三項演算子がありません。
if-elseを使って、最大公約数を計算する方法を書きましょう。
def gcd(x: Int, y: Int): Int = {
if (y == 0) x else gcd(y, x % y)
}
それでは、このメソッドのユニットテストを書きましょう。
@Test
def whenGcdCalledWith15and27__then3 = {
assertEquals(3, gcd(15, 27))
}
4.2. Whileループ
whileループには条件と本体があります。条件が真である間、ループ内で本体を繰り返し評価します。条件は各反復の始めに評価されます。
返すのに役立つものは何もないので、__Unitを返します。
whileループを使って、最大公約数を計算するメソッドを作成しましょう。
def gcdIter(x: Int, y: Int): Int = {
var a = x
var b = y
while (b > 0) {
a = a % b
val t = a
a = b
b = t
}
a
}
それでは、結果を検証しましょう。
assertEquals(3, gcdIter(15, 27))
4.3. ループ中に実行
do whileループはwhileループと似ていますが、ループ条件がループの最後で評価される点が異なります。
do-whileループを使って、階乗を計算するメソッドを書きましょう。
def factorial(a: Int): Int = {
var result = 1
var i = 1
do {
result ** = i
i = i + 1
} while (i <= a)
result
}
次に、結果を確認しましょう。
assertEquals(720, factorial(6))
4.4. 表現のために
for式は、Javaのforループよりもはるかに用途が広いです。
-
単一または複数のコレクションに対して反復できます。さらに、要素を除外したり、新しいコレクションを作成することもできます。
for式を使って、整数の範囲を合計するメソッドを書きましょう。
def rangeSum(a: Int, b: Int) = {
var sum = 0
for (i <- a to b) {
sum += i
}
sum
}
ここで、
aからb
は生成式です。
a
から
b
までの一連の値を生成します。
__i < – a to b
はジェネレータです。これは
i
as
val__を定義し、それにジェネレータ式によって生成された一連の値を割り当てます。
本体は、系列の各値に対して実行されます。
次に、結果を確認しましょう。
assertEquals(55, rangeSum(1, 10))
5.関数
Scalaは関数型言語です。関数はここでは一流の値です – 他の値型と同じように使用できます。
このセクションでは、関数に関連したいくつかの高度な概念、つまりローカル関数、高階関数、無名関数、カレー化について説明します。
5.1. ローカル関数
-
関数の中に関数を定義することができます
それらは入れ子関数またはローカル関数と呼ばれます。ローカル変数と同様に、
それらはそれらが定義されている関数内でのみ可視です。**
それでは、入れ子関数を使ってべき乗を計算する方法を書きましょう。
def power(x: Int, y:Int): Int = {
def powNested(i: Int,
accumulator: Int): Int = {
if (i <= 0) accumulator
else powNested(i - 1, x ** accumulator)
}
powNested(y, 1)
}
次に、結果を確認しましょう。
assertEquals(8, power(2, 3))
5.2. 高階関数
関数は値なので、それらをパラメータとして別の関数に渡すことができます。関数から他の関数を返すこともできます。
-
関数を操作する関数を高階関数と呼びます。それらを使用して、一般化されたアルゴリズムを書くことによってコードの重複を減らすことができます。
それでは、マップを実行し、整数の範囲で演算を減らすための高階関数を書きましょう。
def mapReduce(r: (Int, Int) => Int,
i: Int,
m: Int => Int,
a: Int, b: Int) = {
def iter(a: Int, result: Int): Int = {
if (a > b) {
result
} else {
iter(a + 1, r(m(a), result))
}
}
iter(a, i)
}
ここで、
r
と
m
は
Function
typeのパラメータです。異なる関数を渡すことで、平方和や立方体、階乗など、さまざまな問題を解決できます。
次に、この関数を使用して、整数の2乗を合計する別の関数
sumSquares
を作成しましょう。
@Test
def whenCalledWithSumAndSquare__thenCorrectValue = {
def square(x: Int) = x ** x
def sum(x: Int, y: Int) = x + y
def sumSquares(a: Int, b: Int) =
mapReduce(sum, 0, square, a, b)
assertEquals(385, sumSquares(1, 10))
}
上で、私たちは** 高階関数が多くの小さな使い捨て関数を作成する傾向があることがわかります。無名関数を使うことでそれらを命名することを避けることができます。
5.3. 無名関数
無名関数は、関数として評価される式です。これはJavaのラムダ式に似ています。
上記の例を無名関数を使って書き換えましょう。
@Test
def whenCalledWithAnonymousFunctions__thenCorrectValue = {
def sumSquares(a: Int, b: Int) =
mapReduce((x, y) => x + y, 0, x => x ** x, a, b)
assertEquals(385, sumSquares(1, 10))
}
この例では、
mapReduce
は2つの無名関数を受け取ります。
(x、y)
⇒ x y
および
x ⇒ x ** x
。
-
Scalaはコンテキストからパラメータの型を推測することができます** したがって、これらの関数ではパラメータの型を省略しています。
これにより、前の例に比べてコードがより簡潔になります。
5.4. カレー関数
-
カリー型関数は__def f(x:)のように複数の引数リストを取ります。
Int)__ ** (y:Int) f(5)(6)のように、複数の引数リストを渡すことによって適用されます。
-
これは一連の関数の呼び出しとして評価されます。
** これらの中間関数は単一の引数を取り、関数を返します。
f(5)
のように、部分的に引数リストを指定することもできます。
それでは、例を使ってこれを理解しましょう。
@Test
def whenSumModCalledWith6And10__then10 = {
//a curried function
def sum(f : Int => Int)(a : Int, b : Int) : Int =
if (a > b) 0 else f(a) + sum(f)(a + 1, b)
//another curried function
def mod(n : Int)(x : Int) = x % n
//application of a curried function
assertEquals(1, mod(5)(6))
//partial application of curried function
//trailing underscore is required to
//make function type explicit
val sumMod5 = sum(mod(5)) __
assertEquals(10, sumMod5(6, 10))
}
上で、
sum
と
mod
はそれぞれ2つの引数リストを取ります。 +
mod(5)(6)
のように2つの引数リストを渡します。これは2つの関数呼び出しとして評価されます。まず、
mod(5)
が評価され、関数が返されます。これは、引数
__6で呼び出されます。
__結果として1が得られます。
__mod(5)のように部分的にパラメータを適用することは可能です。
__結果として関数を取得します。
同様に、式
sum(mod(5))
では、
sum
関数に最初の引数だけを渡します。したがって、
sumMod5__は関数です。
アンダースコアは、適用されていない引数のプレースホルダーとして使用されています。コンパイラは関数型が必要であると推測できないので、関数の戻り型を明示的にするために末尾のアンダースコアを使用しています。
5.5. 名前別パラメータ
関数は、値による方法と名前による方法の2つの方法でパラメータを適用できます。値による引数は、呼び出し時に一度だけ評価されます。対照的に、名前による引数は、参照されるたびに評価されます。 by-name引数が使用されていない場合は、評価されません。
-
Scalaはデフォルトで値によるパラメータを使います。パラメータタイプの前に矢印(⇒)がある場合は、by-nameパラメータに切り替わります。
それでは、whileループを実装するためにそれを使用しましょう:
def whileLoop(condition: => Boolean)(body: => Unit): Unit =
if (condition) {
body
whileLoop(condition)(body)
}
上記の関数が正しく機能するためには、
condition
と
body
の両方のパラメーターを、参照するたびに評価する必要があります。したがって、それらを名前によるパラメータとして定義しています。
6.クラス定義
class
キーワードとそれに続くクラス名でクラスを定義します。
名前の後に、** プライマリコンストラクタパラメータを指定できます。そうすることで、自動的に同じ名前のメンバがクラスに追加されます。
クラス本体では、値、変数、メソッドなどのメンバーを定義します。**
private
または
protected
アクセス修飾子によって変更されていない限り、デフォルトでパブリックです。
スーパークラスのメソッドをオーバーライドするには、
override
キーワードを使用する必要があります。
クラスEmployeeを定義しましょう。
class Employee(val name : String, var salary : Int, annualIncrement : Int = 20) {
def incrementSalary() : Unit = {
salary += annualIncrement
}
override def toString =
s"Employee(name=$name, salary=$salary)"
}
ここでは、
name
、
salary
、および
annualIncrement
の3つのコンストラクタパラメータを指定しています。
name
と
salary
を
val
と
var
のキーワードで宣言しているので、対応するメンバーはpublicです。一方、
annualIncrement
パラメータに
val
または
var
キーワードを使用していません。したがって、対応するメンバーは非公開です。このパラメータにデフォルト値を指定しているので、コンストラクタを呼び出すときに省略することができます。
フィールドに加えて、メソッド
incrementSalary
を定義しています。
このメソッドは公開されています。
次に、このクラスの単体テストを書きましょう。
@Test
def whenSalaryIncremented__thenCorrectSalary = {
val employee = new Employee("John Doe", 1000)
employee.incrementSalary()
assertEquals(1020, employee.salary)
}
6.1. 抽象クラス
クラスを抽象化するためにキーワード
abstract
を使います。それはJavaのそれに似ています。通常のクラスが持つことができるすべてのメンバーを持つことができます。
さらに、抽象メンバーを含めることができます。これらは宣言のみで定義なしのメンバで、定義はサブクラスで提供されています。
Javaと同様に、抽象クラスのインスタンスを作成することはできません。
それでは、抽象クラスを例で説明しましょう。
まず、整数の集合を表す抽象クラス
IntSet
を作成しましょう。
abstract class IntSet {
//add an element to the set
def incl(x: Int): IntSet
//whether an element belongs to the set
def contains(x: Int): Boolean
}
次に、空のセットを表す具象サブクラス
EmptyIntSet
を作成しましょう。
class EmptyIntSet extends IntSet {
def contains(x : Int) = false
def incl(x : Int) =
new NonEmptyIntSet(x, this)
}
次に、別のサブクラス
NonEmptyIntSet
が空でない集合を表します。
class NonEmptyIntSet(val head : Int, val tail : IntSet)
extends IntSet {
def contains(x : Int) =
head == x || (tail contains x)
def incl(x : Int) =
if (this contains x) {
this
} else {
new NonEmptyIntSet(x, this)
}
}
最後に、
NonEmptySet
の単体テストを書きましょう。
@Test
def givenSetOf1To10__whenContains11Called__thenFalse = {
//Set up a set containing integers 1 to 10.
val set1To10 = Range(1, 10)
.foldLeft(new EmptyIntSet() : IntSet) {
(x, y) => x incl y
}
assertFalse(set1To10 contains 11)
}
6.2. 特徴
トレイトはJavaインタフェースに対応していますが、次の点が異なります。
-
クラスから拡張できる
-
スーパークラスメンバーにアクセスできる
-
初期化ステートメントを持つことができます
クラスを定義するときに
trait
キーワードを使用してそれらを定義します。
その上、それらはコンストラクタパラメータを除いて抽象クラスと同じメンバを持つことができます。さらに、それらはミックスインとして他のクラスに追加されることを意図しています。
それでは、例を使って特性を説明しましょう。
最初に、
toString
メソッドが大文字の値を返すようにトレイト
UpperCasePrinter
を定義します。
trait UpperCasePrinter {
override def toString =
super.toString toUpperCase
}
それでは、この特性を
Employee
クラスに追加してテストしましょう。
@Test
def givenEmployeeWithTrait__whenToStringCalled__thenUpper = {
val employee = new Employee("John Doe", 10) with UpperCasePrinter
assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}
クラス、オブジェクト、およびトレイトは、最大で1つのクラスを継承できますが、トレイトはいくつでも継承できます。
7.オブジェクト定義
オブジェクトはクラスのインスタンスです。前の例で見たように、
new
キーワードを使ってクラスからオブジェクトを作成します。
ただし、クラスにインスタンスが1つしかない場合は、複数のインスタンスが作成されないようにする必要があります。 Javaでは、これを実現するためにシングルトンパターンを使用します。
そのような場合には、オブジェクト定義と呼ばれる簡潔な構文があります。クラス定義と似ていますが、1つだけ異なります。
class
キーワードを使う代わりに、
object
キーワードを使います
。そうすることでクラスが定義され、その唯一のインスタンスが遅延的に作成されます。
ユーティリティメソッドとシングルトンを実装するためにオブジェクト定義を使います。
Utils
オブジェクトを定義しましょう。
object Utils {
def average(x: Double, y: Double) =
(x + y)/2
}
ここでは、クラス
Utils
を定義し、その唯一のインスタンスも作成しています。
-
この単独のインスタンスを**
Utils
という名前で参照します。このインスタンスは、最初にアクセスされたときに作成されます。
new
キーワードを使用してUtilsの別のインスタンスを作成することはできません。
それでは、
Utils
オブジェクトの単体テストを書きましょう。
assertEquals(15.0, Utils.average(10, 20), 1e-5)
7.1. コンパニオンオブジェクトとコンパニオンクラス
クラスとオブジェクト定義が同じ名前を持つ場合、それらをそれぞれコンパニオンクラスとコンパニオンオブジェクトと呼びます。同じファイルで両方を定義する必要があります。コンパニオンオブジェクトは、コンパニオンクラスからプライベートメンバーにアクセスできます。
Javaとは異なり、静的メンバはありません。代わりに、静的メンバを実装するためにコンパニオンオブジェクトを使用します。
8.パターンマッチング
-
パターンマッチングは、表現を一連の選択肢と照合します。
** これらはそれぞれキーワード
case
で始まります。この後にパターン、区切り記号(⇒)、および多数の式が続きます。
パターンが一致すると式が評価されます。
以下からパターンを構築できます。
-
ケースクラスコンストラクタ
-
可変パターン
-
ワイルドカードパターン__
-
リテラル
-
定数識別子
ケースクラスを使用すると、オブジェクトに対してパターンマッチングを簡単に実行できます。クラスを定義している間に
case
キーワードを追加して、それをケースクラスにします。
-
したがって、パターンマッチングはJavaのswitch文よりもはるかに強力です。このため、広く使用されている言語機能です。
それでは、パターンマッチングを使ってフィボナッチ法を書きましょう。
def fibonacci(n:Int) : Int = n match {
case 0 | 1 => 1
case x if x > 1 =>
fibonacci (x-1) + fibonacci(x-2)
}
次に、このメソッドの単体テストを書きましょう。
assertEquals(13, fibonacci(6))
9.まとめ
このチュートリアルでは、Scala言語とその主要機能のいくつかを紹介しました。これまで見てきたように、これは命令型、関数型、およびオブジェクト指向プログラミングに対する優れたサポートを提供します。
いつものように、完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-scala[over on GitHub]にあります。