Scalaの基本を学ぶ【その1】

Scala
Tomu Obata
📅2019/5/4

Scalaの基本を後から見直しやすいようにと思い要所要所まとめましたが、とても長い記事となっております。逆引きのように見てもらえると幸いです。

動作環境

Scala:2.12

Scalaとは

Scalaの基本

制御構文

if式

if (条件式) A {else B}

while式

while (条件式) A

for式(flatMapmapwithFilterforeachなどあるが、ここでは基本的な使い方)

for(ジェネレータ1; ジェネレータ2; ... ジェネレータn) A
# ジェネレータ1 = a1 <- exp1; ジェネレータ2 = a2 <- exp2; ... ジェネレータn = an <- expn

for式の例:

for (x <- 1 to 5; y <- 1 until 5) { println("x = " + x + "y =" + y)
}

yieldを使ったfor式の例:

for (e <- List("A", "B", "C", "D", "E")) yield { "Pre" + e
}

match式

マッチ対象の式 match { case パターン1 [if ガード1] =>1 case パターン2 [if ガード2] =>2 case パターン3 [if ガード3] =>3 case ... case パターンN [if ガードN] =>N
}

クラス

フィールド定義

(private[this/package]/protected[package]) (val/var) fieldName: Type = Expression

※ private[this]を付けたフィールドへのアクセスは一般にJVMレベルでのフィールドへの直接アクセスになるため、若干高速

抽象メンバー

その時点では実装を書くことができず、後述する継承の際に、メソッド・フィールドの実装を与えたいという時に、抽象メンバーを定義する。

抽象メンバーのメソッド実装例:

(private[this/package]/protected[package]) def methodName(parameter1: Type1, parameter2: Type2, ...): ReturnType

抽象メンバーのフィールド実装例:

(private[this/package]/protected[package]) (val/var) fieldName: Type

継承

継承する目的

クラスの継承:

class SubClass(....) extends SuperClass { ....
}

オブジェクト

object構文の主な用途

  1. ユーティリティメソッドやグローバルな状態の置き場所
  2. オブジェクトのファクトリメソッド
  3. Singletonパターン

objectの基本構文:

object オブジェクト名 extends クラス名 with トレイト名2 ... { 本体
}

トレイト

トレイトの定義:

trait TraitName { (a filed or a method definition)0回以上の繰り返し
}

トレイトの基本

クラスに比べて以下のような特徴がある

trait TraitA {
    val name: String
    def printName(): Unit = println(name)
}

// クラスにしてnameを上書きする
class ClassA(val name: String) extends TraitA

object ObjectA {
    val a = new ClassA("Objective-C")

    // nameを上書きするような実装を与えてもよい
    val a2 = new TraitA { val name = "Swift" }
}

菱形継承問題を解決するための線形化(linearization)

継承関係が複雑になったメソッドを全て明示的に呼ぶのは大変であるため、トレイトがミックスインされた順番をトレイトの継承順番とみなす線形化という機能がある。

trait TraitA { def greet(): Unit
}
trait TraitB extends TraitA { override def greet(): Unit = println("Good morning!")
}
trait TraitC extends TraintA { override def greet(): Unit = println("Good evening!")
}
class ClassA extends TraitB with TraitC

実行文:

scala> (new ClassA).greet()
Good everning!

トレイトの継承順番が線形化され、後からミックスインしたTraitCが優先されているため、ClassAgreetメソッドの呼び出しでTraitCgreetメソッドが実行されている。

自分型

落とし穴:トレイトの初期化順序

trait A {
    val foo: String
}

trait B extends A {
    val bar = foo + "World"
}

class C extends B {
    val foo = "Hello"

    def printBar(): Unit = println(bar)
}

実行文:

scala> (new C).printBar()
nullWorld

Scalaのクラス・トレントはスーバークラスから順番に初期化される。

そのため、この例では、クラスCはトレイトBを継承し、トレイトBはトレイトAを継承している。

つまり、初期化はトレイトAが一番先に行われ変数barが宣言され、中身は何も代入していないので、nullになる。

次に、トレントBで変数barが宣言され、nullであるfooと"World"という文字列から"nullWorld"という文字列が作られ、変数barに代入される。

トレイトのvalの初期化順序の回避方法

trait A { val foo: String
}
trait B extends A { lazy val bar = foo + "World" //もしくは def bar でもよい
}
class C extends B { val foo = "Hello" def printBar(): Unit = println(bar)
}

実行文:

cala
scala> (new C).printBar()
HelloWorld

barの初期化にlazy valを使うことで、barの初期化が実際に行われるまで遅延されることがなくなる。

型パラメータと変位指定

Scalaでは、何も指定しなかった型パラメータは通常は非変(invariant)になる。

共変(covariant)

配列型はJavaでは共変

Object[] objects = new String[1];
objects[0] = 100;

このJavaのコードはコンパイルは通るが、実行すると、java.lang.ArrayStoreExceptionが発生する。これは、objectsに入っているのが、実際には、Stringの配列(Stringのみを要素として持つ)なのに、2行目でint型(ボクシング変換されてInteger型)の値である100を渡そうとしていることになる。

配列型はScalaでは非変なので、上記のコードをコンパイルするとその時点でエラーが表示される。

関数

Scalaの関数は、単にFunction0 〜 Function22までのトレントの無名サブクラスのインスタンスである。

2つの関数を取って加算した値を返すadd関数の定義例)

val add = new Function2[Int, Int, Int] {
    def apply(x: Int, y: Int): Int = x + y
}

無名関数

先ほどのように関数定義するとコードが冗長になり過ぎる。そのため、Scalaでは、Function0 〜 Function22までのトレントのインスタンスを生成するためのシンタックスシュガーが用意されている。

シンタックスシュガーを使ってadd関数を定義した例:

val add = (x: Int, y: Int) => x + y

実行例:

scala> add(100, 200)
res1: Int = 300

関数のカリー化

カリー化とは、(Int, Int) => Int型の関数のように複数の引数を取る関数があったとき、これをInt => Int => Int型の関数のように1つの引数を取り、残りの引数を取る関数を返す関数のチェインで表現するというもの。

上記のaddをカリー化したaddCurried関数を定義した例:

val addCurried = (x: Int) => ((y: Int) => x + y)

実行例:

scala> addCurried(100)(200)
res1: Int = 300

続きはその2へ!

💡
この記事はこちらのクロスポストです

SHARE

スポンサーリンク