🤔

ScalaのHigher kinded typesを調べてみた

2020/09/26に公開

*Qiitaに掲載していたものと同じ内容をこちらに移転しました(作者は同じです)。

社内勉強会でproperな型, kindなどの言葉を耳にしたので、ちょっと自分なりに調べてみた。
この記事ではあくまでも雰囲気を理解するためのものであり、厳密な説明はしてないのであしからず。

キーワード

調べた際のキーワードは以下

  • higher-kinded type
  • Higher-kind Generics
  • type constructor parameter

*それぞれほぼ同じ意味だけど、呼び方がいろいろあるらしい...(統一して)

型表現の抽象度

型にも抽象度がある(と、自分は勝手に思ってるだけで、厳密な定義方法はちょっとよく分からん)。
どういうことか?具体から抽象へと少しずつ階段を登っていくことにしよう。

具体

例えば、1"hello" というのは、それそのものは非常に具体的な値をもつ。
具体的故に非常にわかりやすい。
これをvalueとか言うらしい。まぁ、値だね。

抽象0階層目

次に少し抽象度を上げてみよう。
先程の1"hello"をもう少し抽象的に表現すると、IntStringList[String]という感じに表現できるだろう。
ここでは具体的な値は一切なくなり、型だけで表現されている。Genericsや型クラスに相当する感じだろうか。
こいつをproper typeというらしい。

orderというのは階層みたいな感じなんだけど、ここでは「抽象度の階層」みたいなイメージを持っておくと良いかもしれない。
valueがorder 0なんじゃないの?とか思うけど、たぶんvalueは抽象として扱うには具体的すぎるんじゃなかろうか。

抽象1階層目

更に抽象度をあげてみる。

IntやStringはそれ以上は抽象にならなさそうだけど、何らかの文脈をもつ型はできそう。
つまり、ListとかOptionとかが対象。

List[String]はproper typeだけど、List[_] とするとより抽象度が上がったのではないだろうか。

_ ってなんやねん....。

_ とすることで、ListやOptionの中に入る型を限定していないことになる(IntでもBooleanでもOKということ)。
パターンマッチングとかで case _ => みたいにするのと同じ感じで思っておくとわかりやすいかも。

List[]とかOption[]とかのことをfirst-orderというらしい。

抽象2階層目

で、first-orderを更に抽象的に表現できないかと考えてみる。

コイツらの共通点はなんだろう?

ざっくりいうと、「何らかの文脈をもち、その中に何らかの型を保持する型」みたいな感じだろうか。
これをF[_]という形で表せる。

「何らかの文脈(F)が、その中に何らかの型を保持している」みたいな感じ。
コイツを Higher kinded typesというらしい(呼び方は他にもある)。

まとめると以下の感じ。

- value proper type first-order Higher kinded types
"Hello", 1, List(10) Int, String, List[Int] List[_ ], Option[ _ ] F[ _ ]
order - 0 1 2

例を見てみる

さて、型にも抽象度のレベルがあることを知ったところで以下のtraitを見てみよう。

trait Bindable[F[_], +A] {
  def map[B](f: A => B): F[B]
}

F[_] という、モロそれなやつがいる。

Bindableは文脈付きの型とAを使い、mapを通じてF[B]を作り出す。
F[_]List[_]で、A, BがInt とすると、List[Int]でのmapみたいなイメージだろうか。

もう一つ例を出す。
この例はScalaのHigher-kind Genericsについてから拝借

trait AttrHelper[+Holder[X]]

この場合には +Holder[X]がキモになる。
これがただの型クラスだとAttrHelper[+G]みたいな感じになるのだろうが、今回の例ではそうはなってない。
Holderは、ある型Xを型パラメータとして取って何かの型を作り出すものという感じ。

メリット

さて、抽象化の話をしていることはなんとなく分かったんだけど、これをして何が嬉しいのだろう?

自分の理解では、抽象化によって本質的な共通点を抽出できることがメリットなんだろうと思う。

例えば、"Hello", List(1, 2)ではそれぞれの共通点を見つけるのが難しいし、見つけられたとしてもあまり汎用的なものではないかもしれない。
List[_]Option[_]になると、それぞれどことなく似通った点を持っていることに気づくかもしれない。

F[_]だと、もうList[_]Option[_]などの共通点の塊になる。
F[_]がもつ何らかの事項は、List[_]Option[_]などにそのまま適応できるのだ。
その共通点をtraitなどで実装すれば、ListやOptionなどに対して共通のインターフェースをもたせることができそう、というわけだ。

使ってみる

というわけでざっくり使ってみる。

ListやOptionをtupleとして合成したい処理を書きたいとする。

def tupleList[A, B](fa: List[A], fb: List[B]): List[(A, B)] = ???
def tupleOpt[A, B](fa: Option[A], fb: Option[B]): Option[(A, B)] = ???

ListとOptionという、扱う型は違うけれど、やってることは同じ感じになるので、以下のように抽出できないだろうか?

def tupleF[F[_], A, B](fa: F[A], fb: F[B]): F[(A, B)] = ???

implicitを使ってやってみる

// type class
trait Bind[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// type class instances
implicit val bindOption = new Bind[Option] {
  override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = ???
  override def map[A, B](fa: Option[A])(f: A => B): Option[B] = ???
}

implicit val bindList = new Bind[List] {
  override def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = ???
  override def map[A, B](fa: List[A])(f: A => B): List[B] = ???
}

// type class method
def tupleCT[F[_], A, B](fa: F[A], fb: F[B])(implicit F: Bind[F]): F[(A, B)] =
  F.flatMap(fa){ a =>
    F.map(fb)((a, _))
  }

// 呼び出し
val a: Option[String] = Some("a")
val b: Option[String] = Some("b")

tupleCT(a, b)

まとめ

型にも抽象度のレベルがあり、抽象度を上げると見えてくる共通点が出てくるというのを知った。
静的型付け言語を使う上では抽象と具象の行き来が重要なのであろう。

参考

Discussion