Scalaの紹介
1. 序章
このチュートリアルでは、Java仮想マシンで実行される主要言語の1つであるScalaについて説明します。
まず、値、変数、メソッド、制御構造などのコア言語機能から始めます。 次に、高階関数、カリー化、クラス、オブジェクト、パターンマッチングなどの高度な機能について説明します。
JVM言語の概要については、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>
次に、コードのコンパイル、テスト、実行、文書化のために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には、scala-langおよびscala-maven-pluginの最新のアーティファクトがあります。
最後に、ユニットテストにJUnitを使用します。
3. 基本的な機能
このセクションでは、例を通して基本的な言語機能を調べます。 この目的のために、Scalaインタープリターを使用します。
3.1. 通訳者
インタプリタは、プログラムや式を作成するためのインタラクティブなシェルです。
それを使用して「helloworld」を印刷してみましょう。
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ループとdowhileループ
- 表現のために
- 表現してみてください
- マッチ式
Javaとは異なり、continueまたはbreakキーワードはありません。 returnキーワードがあります。 ただし、使用は避けてください。
switch ステートメントの代わりに、match式によるパターンマッチングがあります。 さらに、独自のコントロール抽象化を定義できます。
4.1. if-else
if-else式はJavaに似ています。 else部分はオプションです。 複数のif-else式をネストできます。
式なので、値を返します。 したがって、Javaの三項演算子(?:)と同様に使用します。 実際、言語には三項演算子がありません。
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からb ジェネレーターです。 iを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タイプのパラメータです。 さまざまな関数を渡すことで、2乗和や3乗和、階乗など、さまざまな問題を解決できます。
次に、この関数を使用して、整数の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)が評価され、関数が返されます。 これは、順番に、引数で呼び出されます
次のようにパラメータを部分的に適用することが可能です。
同様に、式 sum(mod(5))_ では、最初の引数のみをsum関数に渡します。 したがって、sumMod5は関数です。
アンダースコアは、適用されていない引数のプレースホルダーとして使用されます。 コンパイラーは関数型が予期されていることを推測できないため、末尾の下線を使用して関数の戻り型を明示的にしています。
5.5. 名前によるパラメータ
関数は、値と名前の2つの異なる方法でパラメーターを適用できます。関数は、呼び出し時に1回だけ値による引数を評価します。 対照的に、参照されるたびに名前による引数を評価します。 by-name引数が使用されていない場合、評価されません。
Scalaはデフォルトで値によるパラメーターを使用します。 パラメータタイプの前に矢印(=>)が付いている場合は、名前によるパラメータに切り替わります。
それでは、これを使用してwhileループを実装しましょう。
def whileLoop(condition: => Boolean)(body: => Unit): Unit =
if (condition) {
body
whileLoop(condition)(body)
}
上記の関数が正しく機能するためには、パラメータconditionとbodyの両方が参照されるたびに評価される必要があります。 したがって、これらを名前によるパラメーターとして定義しています。
6. クラス定義
class キーワードの後にクラスの名前を付けて、クラスを定義します。
名前の後に、
クラス本体では、値、変数、メソッドなどのメンバーを定義します。 プライベートまたは保護されたアクセス修飾子によって変更されない限り、デフォルトでパブリックになります。
スーパークラスのメソッドをオーバーライドするには、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キーワードで宣言しているため、対応するメンバーは公開されています。 一方、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キーワードを使用します。 さらに、コンストラクターパラメーターを除いて抽象クラスと同じメンバーを持つことができます。 さらに、それらはミックスインとして他のクラスに追加されることを意図しています。
それでは、例を使用して特性を説明しましょう。
まず、特性 UpperCasePrinter を定義して、toStringメソッドが大文字の値を返すようにします。
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キーワードを追加して、ケースクラスにします。
したがって、パターンマッチングは、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言語とその主要な機能のいくつかを紹介しました。 これまで見てきたように、命令型、機能型、およびオブジェクト指向プログラミングの優れたサポートを提供します。
いつものように、完全なソースコードはGitHubのにあります。