Lambdaカクテル

京都在住Webエンジニアの日記です

Scalaで型パラメータ付きクラス(トレイト)と抽象型付きクラス(トレイト)復習したところ全称型と存在型に出会った

だいたい同じことができる(ような気がする)別々の2つの言語機能についてよくわかっていないので書く。

// 筆者は型初心者。

型パラメータ

traitには型パラメータを設けることができる。いわゆるジェネリクスというやつである。

trait Plus[A] {
  def plus(a2: A): A
}

extendすることで具体的な型のインスタンスを得ることができる:

class p1(a1: Int) extends Plus[Int] {
  def plus(a2: Int): Int = a1+a2
}

new p1(3).plus(4) // => 7

せやなという感じである。

抽象型

ところで,抽象型を使っても同じようなことができる。

trait Plus2 {
  type A
  def plus(a2: A): A
}

こちらもインスタンス化してみよう:

class p2(a1: Int) extends Plus2 {
  type A = Int
  def plus(a2: A): A = a1+a2
}

new p2(3).plus(4) // => 7

こちらもせやなという感じである。

違い 何

だいたい同じようなことができるのだが,型パラメータはupper/lower boundを指定できる:

trait Doubler[A <: Seq[Int]] { // Seq[Int]の子じゃないと受け付けないよ
  def double: A
}

class d(lis: List[Int]) extends Doubler[List[Int]] {
  def double: List[Int] = lis.map(_*2)
}

new d(List(1,2,3)).double // => List(2,4,6)

ところが抽象型でも同じことができる。

trait Doubler2 {
  type A <: Seq[Int]
  def double: A
}

class d2(lis: List[Int]) extends Doubler2 {
  type A = List[Int]
  def double: List[Int] = lis.map(_*2)
}

new d2(List(1,2,3)).double

全然違いがわからなくなってしまった。ここで答え合わせをしよう。

Stack Overflow先生に教えてもらう

雑に検索したら一発で出てきた。

stackoverflow.com

要点は以下の通り。

  • 「フィールド」「メソッド」「型」の抽象化にあたって,「パラメータで渡す」「抽象メンバにする」という2つのアプローチが考えられる
  • Javaは型とフィールドをパラメータで渡せるようにし,メソッドは抽象メンバとして実装できるようにした
  • Scalaはどちらでも良いように設計したので,パラメータにもできるし,抽象メンバにもできるようになっている
  • パラメータ化は,難しいことをやろうとすると爆発的にパラメータ数が増加し,制御が難しくなるという問題を抱えている
  • 抽象メンバではこの問題は起こりにくい

そういえばパラメータではなくメンバとして渡していく,というのはCake Patternにも共通していて面白いと思う。

そして,型パラメータはクライアントコードからは見えなくなるという点も違うようだ。抽象型は継承した先で参照・上書きできるが,型パラメータは隠蔽されて見えなくなってしまう。また型パラメータは使う時に名前が見えなくなる。

// 型パラメータ版
// library code

trait Animal[A] {
  def eat(something: A): Unit
}

class Grass {}

class Cow extends Animal[Grass] {
  def eat(g: Grass): Unit = { println("moo") }
}

// client code

object BlackCow extends Cow { // Aはもう見えない!!
  override def eat(g: Grass): Unit = { println("moomoo") }
}

BlackCow.eat(new Grass)
// 抽象型版
// library code

trait Animal {
  type Food
  def eat(something: Food): Unit
}

class Grass {}

class Cow extends Animal {
  type Food <: Grass // ここでUpper boundを指定しておくと...
  def eat(g: Food): Unit = { println("moo") }
}

// client code

class BlackGrass extends Grass {}

object BlackCow extends Cow {
  override type Food = BlackGrass // 後から上書きできる
  override def eat(g: Food): Unit = { println("moomoo") }
}

BlackCow.eat(new BlackGrass)

全称型と存在型

ところで型パラメータと抽象型は全称型と存在型とに対応しているらしい(cf. p.33)。

www.slideshare.net

んーわからん。

  • 型パラメータAは全ての型Aについてモジュール内の関数に型が付く
  • 抽象型Aはある型Aについてモジュール内の関数に型が付く

?????

abstract class JSON1[A] {
  def toJSON(x: A): String
}

class ConcreteJSON1[A] extends JSON1[A] {
  def toJSON(x: A): String = {
    // 全ての型を相手にしなければならない??
    ""
  }
}

abstract class JSON2 {
  type X
  def toJSON(x: X): String
}

class ConcreteJSON2 extends JSON2 {
  type X = Int // ここでIntに絞れるということが言いたい??
  def toJSON(x: X): String = x.toString()
}

// ところで

class ConcreteJSON1Int extends JSON1[Int] { // 普通にここで絞れるじゃん
  def toJSON(x: Int): String = x.toString()
}

forSomeなにもわからない。

おしまい