2018/04/20

 Scalaの型と型関連の機能


  1. Scalaと型
  2. 型クラス
  3. 構造的部分型(Structural subtyping)
  4. Scalaの3つのdependent type
  5. まとめ

Scalaについて勉強した時のメモ(その6)です。Scalaの型の機能とそれに関連するテクニックについて。

※ 元々はmarkdownで書いていたテキストの転載。

Scalaと型

  • Scalaの型推論は漸進的型付と呼ばれ、基本的に前から推論していく。
    • (余談)HaskellやOCamlの型推論は、Hindley-Minler(の派生)と呼ばれる推論方式。 この方法は、最も一般的な型を自動的に導出していく手法で、通常の場合、いわゆる型注釈(型ヒント)に相当するものが不要。
  • 型注釈: 変数や引数などに対する型の指定。いわゆる、val x: String = 〜のコロンの後ろの型指定のこと。
  • 型パラメータ: Javaで言う所のジェネリクス。
    • trait A[B] { def b():B; }B
    • 型パラメータで指定できる共変、反変、非変については、型パラメータと変位指定 - dwango on GitHub を参照。以下、自分用のメモ。
      • 共変([+B]): A extends Bの時のみ、val a:G[B] = b:G[A]が可。
      • 反変([-B]): A extends Bの時のみ、val a:G[A] = b:G[B]が可。
      • 非変([B]): A = Bの時のみ、val a:G[A] = b:G[B]が可。
      • 上界([B <: A]): BがAを継承の性質。
      • 下界([B >: A]): BがAのスーパークラスである性質。
    • 型パラメータに様々な制約を付ける事で、クラス、インターフェースなしにジェネリックな関数を定義できる。(構造的部分型を参照)
  • 型エイリアス: 型に別名を付けることができる。型定義の長さが絶望的に長くなった時に有効。
    • type String3 = (String, String, String)
  • 型コンストラクタ: 型を引数にとり別の型を生成する型。
    • プログラミングでよく目にするものとしては、Option, Either, Futureなどが典型的。
    • 型パラメータが引数にとる。
    • Option[+A]という型コンストラクタに対して、Option[String]という型を定義する時などに使われる。
    • 他の関数型言語だとFunctorなどがよく出てくる。

型クラス

  • "既存の型に後付けするタイプのインターフェース"(参考文献から引用)
  • ある型がどのような振る舞いをするかまとめた物。
    • 上記のような言い方をすると結局Javaのインターフェースと一緒じゃんって言われる。。。
    • 型クラスの定義の流れ。
      1. 振る舞いをまとめたtrait A[T]を作る。(Tは型パラメータ。型クラスは必ず型パラメータを持つ)
      2. trait A[T](型クラス)のインスタンス(実装)X, Y, Zを作り、implicitに定義する。
      3. Aで使っている関数を使ったコードBを実装する。 この時、trait A[T]のインスタンス(実装)X, Y, Zをimplicitに切り替える。
      4. 以降、コードBに型ごとに機能を追加したい場合は、trait Aの実装を追加することで、コードBの機能が様々な型で使えるようになる。
    • Javaで言うと、Interface(や抽象クラス)のインスタンスが暗黙に選択される(切り替えられる)イメージ。
  • 型クラスを使用したコード側で型クラス(の定義)に紐づく型クラスのインスタンス(実装)を切り替える。
    • (Scalaの場合、)型クラスはimplicit(暗黙のパラメータ)で型クラスの実装を切り替える。
    • 型で処理を切り替える所がポイント。

class Firefox {
}

class Chrome {
}

trait BrowserShows[A] {
  def show: String
}

implicit val firefox = new BrowserShows[Firefox] {
  def show: String = "Firefoxで表示"
}

implicit val chrome = new BrowserShows[Chrome] {
  def show: String = "Chromeで表示"
}

def exec[B](browser: B)(implicit s: BrowserShows[B]): String = s.show

def main(args: Array[String]): Unit = {
  println(exec(new Chrome()))
  println(exec(new Chrome()))
  println(exec(new Firefox()))
}

構造的部分型(Structural subtyping)

  • 特定の性質を持った型を型パラメータとして定義(宣言)できる。
    • 部分的な型を表すという意味では、traitに近いが、オブジェクトを使用する側でのみ必要な型を定義するという点がtraitとは異なる。 traitによる定義は、あくまでクラスの性質を表すが、構造的部分型は引数や変数に代入可能な一般的なオブジェクトの型を表す (その型が必要な場面でのみ表現することができる)。
    • 動的型付け言語におけるDuck-typingの性質を静的型付言語で使用したい場合に使用できる。
    • その場面のコードの変数で最も一般的な型を必要な箇所で定義することで、静的に型付したままDuck-typing的な性質を実現する。
  • 動的型付け言語(Ruby, Pythonなど)は、名前でメソッドを引っ張ってくる。
class A:
    def func1(self, i):
        〜

class B:
    def func1(self, i):
        〜

def func2(objX):
  objX.func1(1)
  〜

func2(A())
func2(B())

となるような、一般的なfunc2を定義できる。Scalaでも、func1を持つようなオブジェクトを一般的に引き受けるような関数を定義したい。

class Aやclass Bの定義を変更することなしに。そして、関数func1を持つオブジェクトは知らされることなく常に増えていく。。。 もちろん、(Javaの)interfaceや(Scalaの)traitだとfunc2に与えられるオブジェクトのクラス全てにinterfaceを付けなければいけない。

例えば、idとnameを持つようなRow型について考える。

case class AbcRow(id: Long, name: String, paramA: String, paramB: String)
case class DefRow(id: Long, name: String, idA: Int, flagB: Boolean)
case class GhiRow(id: Long, name: String, id: Long)
case class JklRow(id: Long, name: String, messageA: String)

このRowのリスト(Seq[AbcRow]やSeq[DefRow]のようなオブジェクト)から、 idとnameのタプルのリストを抽出する一般的な関数を定義したい。。。

この場合、次のような関数を定義できる。

def getIdName[R <: {val id: Long; val name: String;}](rows: Seq[R]): Seq[(Long, String)] =
    rows.map { row => (row.id, row.name) }

これは次のようにも書ける。

def getIdName(rows: Seq[{val id: Long; val name: String;}]): Seq[(Long, String)] =
    rows.map { row => (row.id, row.name) }

Long型のidとString型のnameを持つ最も一般的な型を引数にもち、その型のインスタンスからidとnameを抽出する関数を定義できる。 他の型(クラス)定義を書き換えること無く、アドホックに静的な型チェックを維持した関数を定義できる。 (Duck-typingの性質を引き継げる。) 次のように実行する。

scala> val row = Seq(AbcRow(1, "a", "paramA", "paramB"), AbcRow(2, "b", "paramA", "paramB"), AbcRow(3, "c", "paramA", "paramB"));
row: Seq[AbcRow] = List(AbcRow(1,a,paramA,paramB), AbcRow(2,b,paramA,paramB), AbcRow(3,c,paramA,paramB))
scala> getIdName(row)
res3: Seq[(Long, String)] = List((1,a), (2,b), (3,c))
  • 条件を満たす型(データ型)を定義しておき、テンプレートを書く。
  • (余談)この辺の型の推論をOCamlだと自動でやってくれる。。。Scalaは割と自分で書かないといけないという面倒くささはある。
  • 上記以外だとローンパターン(ローンパターン自体は、構造的部分型の特殊な利用パターンの一種)などで頻繁に使うことができる。

Scalaの3つのdependent type

まとめ

  • 型推論という点ではHaskellやOCamlにはかなわない(?)ものの、様々なケースで静的に型付けしながらも柔軟な型定義を実現するための 仕組みが備わっている事がわかる。
  構造的部分型 Scala 型クラス 型システム