Scalaの紹介

Scalaの概要

1. 前書き

このチュートリアルでは、Java仮想マシンで実行される主要言語の1つであるScalaについて説明します。

まず、値、変数、メソッド、制御構造などのコア言語機能から始めます。 次に、高階関数、カリー化、クラス、オブジェクト、パターンマッチングなどの高度な機能について説明します。

JVM言語の概要を取得するには、quick guide to the JVM Languagesを確認してください。

2. プロジェクトのセットアップ

このチュートリアルでは、https://www.scala-lang.org/download/からの標準のScalaインストールを使用します。

まず、pom.xmlにscala-libraryの依存関係を追加しましょう。 このアーティファクトは、言語の標準ライブラリを提供します。


    org.scala-lang
    scala-library
    2.12.7

次に、コードのコンパイル、テスト、実行、文書化のためにscala-maven-pluginを追加しましょう。


    net.alchim31.maven
    scala-maven-plugin
    3.3.2
    
        
            
                compile
                testCompile
            
        
    

Mavenには、scala-langscala-maven-pluginの最新のアーティファクトがあります。

最後に、ユニットテストにJUnitを使用します。

3. 基本的な機能

このセクションでは、例を通して基本的な言語機能を調べます。 この目的のためにScala interpreterを使用します。

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. 式

Any computable statement is an expression

いくつかの式を書いて、その結果を見てみましょう。

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

上で見ることができるように、every expression has a value and a type

If an expression does not have anything to return, it returns a value of type Unit。 このタイプの値は()の1つだけです。 これは、Javaのvoidキーワードに似ています。

3.3. 値の定義

キーワードvalは、値を宣言するために使用されます。

式の結果に名前を付けるために使用します:

scala> val pi:Double = 3.14
pi: Double = 3.14

scala> print(pi)
3.14

そうすることで、結果を複数回再利用できます。

Values are immutable。 したがって、それらを再割り当てすることはできません。

scala> pi = 3.1415
:12: error: reassignment to val
       pi = 3.1415
         ^

3.4. 変数定義

値を再割り当てする必要がある場合は、代わりに変数として宣言します。

変数の宣言に使用されるキーワードvar is:

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ループとdo whileループ

  • 表現のために

  • 表現を試す

  • 一致表現

Javaとは異なり、continueまたはbreakキーワードはありません。 returnキーワードがあります。 ただし、使用しないでください。

switchステートメントの代わりに、match式によるパターンマッチングがあります。 さらに、独自の制御抽象化を定義できます。

4.1. if-else

if-else expressionはJavaに似ています。 else部分はオプションです。 複数のif-else式をネストできます。

Since it is an expression, it returns a value。 したがって、Javaの三項演算子(?:)と同様に使用します。 実際、言語does not have have the ternary operator.

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ループには条件と本体があります。 条件がtrueの間、ループ内の本体を繰り返し評価します。条件は各反復の開始時に評価されます。

返すのに役立つものがないため、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. Whileループ

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 to bはジェネレータ式です。 aからbまでの一連の値を生成します。

i ← a to b はジェネレーターです。 asvalを定義し、ジェネレータ式によって生成された一連の値を割り当てます。

本体はシリーズの各値に対して実行されます。

次に、結果を確認しましょう。

assertEquals(55, rangeSum(1, 10))

5. 関数

Scalaは関数型言語です。 ここでの関数はファーストクラスの値です。他の値型と同様に使用できます。

このセクションでは、関数に関連するいくつかの高度な概念(ローカル関数、高階関数、無名関数、カリー化)について説明します。

5.1. ローカル関数

We can define functions inside functions.これらはネストされた関数またはローカル関数と呼ばれます。 ローカル変数と同様に、they are visible only within the function they are defined in.

それでは、入れ子関数を使用して電力を計算するメソッドを作成しましょう。

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. 高階関数

関数は値なので、それらをパラメーターとして別の関数に渡すことができます。 関数が別の関数を返すようにすることもできます。

We refer to functions which operate on functions as higher-order functions.これらにより、より抽象的なレベルでの作業が可能になります。 それらを使用して、一般化されたアルゴリズムを記述することにより、コードの重複を減らすことができます。

それでは、マップを実行し、整数の範囲で操作を減らす高階関数を記述しましょう。

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)
}

ここで、rmFunctionタイプのパラメーターです。 異なる関数を渡すことにより、平方和や立方体、階乗などのさまざまな問題を解決できます。

次に、この関数を使用して、整数の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))
}

上記では、higher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.がわかります

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(x, y) ⇒ x + yx ⇒ x * xの2つの無名関数を受け取ります。

Scala can deduce the parameter types from context.したがって、これらの関数ではパラメーターのタイプを省略しています。

これにより、前の例よりも簡潔なコードが作成されます。

5.4. カリー化関数

A curried function takes multiple argument lists, such as def f(x: Int)(y:Int)。 f(5)(6)のように、複数の引数リストを渡すことで適用されます。

It is evaluated as an invocation of a chain of functions. これらの中間関数は単一の引数を取り、関数を返します。

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))
}

上記では、summodはそれぞれ2つの引数リストを取ります。 mod(5)(6)のような2つの引数リストを渡します。 これは、2つの関数呼び出しとして評価されます。 最初に、mod(5)が評価され、関数が返されます。 これは、引数6. で呼び出されます。結果として1が得られます。

mod(5). のようにパラメータを部分的に適用することが可能です。結果として関数が得られます。

同様に、式sum(mod(5)) _では、最初の引数のみをsum関数に渡します。 したがって、sumMod5は関数です。

下線は、適用されない引数のプレースホルダーとして使用されます。 コンパイラは関数型が予期されることを推測できないため、末尾のアンダースコアを使用して、関数の戻り型を明示的にしています。

5.5. 名前別パラメーター

関数は、値と名前で2つの異なる方法でパラメーターを適用できます。値ごとの引数は、呼び出し時に一度だけ評価されます。 対照的に、参照されるたびに名前による引数を評価します。 by-name引数が使用されない場合、評価されません。

Scalaはデフォルトで値によるパラメーターを使用します。 パラメータタイプの前に矢印(⇒)が付いている場合は、名前によるパラメータに切り替わります。

それでは、これを使用してwhileループを実装しましょう。

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

上記の関数が正しく機能するためには、パラメータconditionbodyの両方が参照されるたびに評価される必要があります。 したがって、名前によるパラメータとして定義しています。

6. クラス定義

classキーワードの後に​​クラスの名前を付けて、クラスを定義します。

名前の後、we can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

クラス本体では、値、変数、メソッドなどのメンバーを定義します。 They are public by default unless modified by the private or protected access modifiers.

スーパークラスのメソッドをオーバーライドするには、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)"
}

ここでは、namesalary、およびannualIncrementの3つのコンストラクターパラメーターを指定しています。

namesalaryvalvarのキーワードで宣言しているため、対応するメンバーは公開されています。 一方、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では、Singletonパターンを使用してこれを実現します。

このような場合、オブジェクト定義と呼ばれる簡潔な構文がありますが、クラス定義に似ていますが、1つ違いがあります。 Instead of using the class keyword, we use the object keyword。 そうすることでクラスを定義し、その唯一のインスタンスを遅延的に作成します。

オブジェクト定義を使用して、ユーティリティメソッドとシングルトンを実装します。

Utilsオブジェクトを定義しましょう:

object Utils {
  def average(x: Double, y: Double) =
    (x + y) / 2
}

ここでは、クラスUtilsを定義し、その唯一のインスタンスも作成しています。

We refer to this sole instance using its nameUtils。 このインスタンスは、最初にアクセスしたときに作成されます。

newキーワードを使用してUtilsの別のインスタンスを作成することはできません。

それでは、Utilsオブジェクトの単体テストを作成しましょう。

assertEquals(15.0, Utils.average(10, 20), 1e-5)

7.1. コンパニオンオブジェクトとコンパニオンクラス

クラスとオブジェクト定義の名前が同じ場合、それらをそれぞれコンパニオンクラスおよびコンパニオンオブジェクトと呼びます。 同じファイルで両方を定義する必要があります。 コンパニオンオブジェクトは、コンパニオンクラスからプライベートメンバーにアクセスでき、その逆も可能です。

Javaとは異なり、we do not have static members.代わりに、コンパニオンオブジェクトを使用して静的メンバーを実装します。

8. パターンマッチング

Pattern matching matches an expression to a sequence of alternatives. これらはそれぞれキーワード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言語とその主要機能の一部を紹介しました。 これまで見てきたように、命令型、関数型、オブジェクト指向プログラミングの優れたサポートを提供します。

いつものように、完全なソースコードはover on GitHubにあります。