Scalaの基本(その1)制御構文・クラス・関数
Scalaの基本を網羅!if式・match式・クラス・トレイト・関数などをサンプルコード付きで解説。初心者にもわかりやすい構成で逆引きにも最適!
Scalaの基本を後から見直しやすいようにと思い要所要所まとめましたが、とても長い記事となっております。逆引きのように見てもらえると幸いです。
動作環境
Scala:2.12
Scalaとは
- オブジェクト指向プログラミングと関数型プログラミングの統合
- Javaと互換性があり、ライブラリも使用可能
Scalaの基本
- 変数には、
valとvarの2種類がある valは値の再代入が不可。varは値の再代入が可能。基本的にはvalのみでプログラミングする- Swiftでいうところの、
let(再代入不可)とvar(再代入可能)
- Swiftでいうところの、
制御構文
if式
if (条件式) A {else B}while式
while (条件式) Afor式(flatMap・map・withFilter・foreachなどあるが、ここでは基本的な使い方)
for(ジェネレータ1; ジェネレータ2; ... ジェネレータn) A
# ジェネレータ1 = a1 <- exp1; ジェネレータ2 = a2 <- exp2; ... ジェネレータn = an <- expnfor式の例:
for (x <- 1 to 5; y <- 1 until 5) { println("x = " + x + "y =" + y)
}1 to 5は1から5まで(5を含む)の範囲1 until 5は1から5まで(5を含まない)の範囲
yieldを使ったfor式の例:
for (e <- List("A", "B", "C", "D", "E")) yield { "Pre" + e
}for構文はyieldキーワードを使うことで、コレクションの要素を加工して返すという全く異なる要素に使うことが可能
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継承
継承する目的
- スーパークラスの実装をサブクラスでも使うことで実装を再利用すること
- 複数のサブクラスが共通のスーパークラスのインターフェースを継承することで処理を共通化すること(サブタイピング・ポリモーフィズムと呼ぶ)
- Scalaでは、トレイトという仕組みで複数の実装の継承を実現している。(下記のトレイトを参照)
クラスの継承:
class SubClass(....) extends SuperClass { ....
}オブジェクト
- Scalaでは、全ての値がオブジェクトで、全てのメソッドは何らかのオブジェクトに所属している
object構文の主な用途
- ユーティリティメソッドやグローバルな状態の置き場所
- オブジェクトのファクトリメソッド
- Singletonパターン
objectの基本構文:
object オブジェクト名 extends クラス名 with トレイト名2 ... { 本体
}- クラスと同じファイル内、同じ名前で定義されたシングルトンオブジェクトは、コンパニオンオブジェクトと呼ばれる
- コンパニオンオブジェクトは、対応するクラスに対して特権的なアクセス権を持つ
トレイト
- Scalaのトレイトはクラスからコンストラクタを定義する機能を抜いたようなもの
トレイトの定義:
trait TraitName { (a filed or a method definition)の0回以上の繰り返し
}トレイトの基本
クラスに比べて以下のような特徴がある
- 複数のトレイトを1つのクラスやトレイトにミックスインできる
- 複数のクラスを継承させたい場合は、クラスをトレイトにする必要がある
- 直接インスタンス化できない
- トレントを使うときは通常、それを継承したクラスを作る。これは、トレントが単体で使われることを想定していないための制限である。
- クラスパラメータ(コンストラクタの引数)を取ることができない
- トレイトに抽象メンバを持たせることで値を渡すことが可能
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が優先されているため、ClassAのgreetメソッドの呼び出しでTraitCのgreetメソッドが実行されている。
自分型
- 自分型を使う場合、抽象トレイトを指定し、後から実装を追加するという形になる
- このように後(または外)から利用するモジュールの実装を与えることを「依存性の注入」と呼ぶ
- 型の循環参照(相互参照)を許す
落とし穴:トレイトの初期化順序
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()
nullWorldScalaのクラス・トレントはスーバークラスから順番に初期化される。
そのため、この例では、クラス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()
HelloWorldbarの初期化にlazy valを使うことで、barの初期化が実際に行われるまで遅延されることがなくなる。
型パラメータと変位指定
Scalaでは、何も指定しなかった型パラメータは通常は非変(invariant)になる。
共変(covariant)
配列型はJavaでは共変
- 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