Scalaにおける型パラメータの部分適用 [({type F[X] = G[A,X]})#F] について
Tumblrから出戻ってきました。
8/4のLL DecadeのLT大会に出るのでぜひお越しください。
さて、モナってますか? scalazなどでよく出現する[({type F[X] = G[A,X]})#F]のようなコードですが、これが何を意味しているのか最近やっと理解できたので、久しぶりにScalaの事書きます。
この記事はhigher kinded type(高階型)を理解していることが前提です。
結論からいうと、型パラメータの部分適用を行うためのテクニックです。以下のサンプルコードはscalazを使ってます。
用語
まず、この記事で使う用語を定義します。
- higher kinded type(高階型)
- 「いくつかの型パラメータを取る型コンストラクタ」のこと。
- 例えば、ListはList[Int]のように、「型パラメータをひとつ取る型」なので高階型。
- Eitherは、Either[Throwable, String]のように「型パラメータを二つとる」高階型。
- 高階型を型パラメータに取る関数は、"def foo[F[_,_]]"のように定義する。この例では、Fは「型パラメータを二つとる」高階型。
- kind
- 高階型が、いくつの型パラメータを取るかを表す。いわば、型コンストラクタのシグニチャ。
- Option, Listのような「型パラメータを一つ取る高階型」は、kindを「* -> *」と表記する
- Map, Eitherのような「型パラメータを二つ取る高階型」は、kindを「* -> * -> *」と表記する
- 高階型を型パラメータにとるような関数を呼び出すときには、kindが一致する型を渡さなければならない。上記の例のfoo関数には、kindが"* -> *_-> *"である高階型を渡さないとコンパイルエラーとなる
- 高階型ではない普通の型(StringとかIntとか)のkindは"*"。List[Int]のように、高階型に具体的な型を与えた型のkindも、同じく"*"
基本編
では、サンプルコードです。scalazには、pureというメソッドがあります。これはkindが1(* -> *)の高階型でかつPureのインスタンスである型を引数にとり、値を包み込む関数です。
import scalaz._;import Scalaz._ scala> 1.pure[Option] res1: Option[Int] = Some(1) scala> "foo".pure[List] res2: List[java.lang.String] = List(foo) scala> "oppai".pure[Either] <console>:14: error: Either takes two type parameters, expected: one "oppai".pure[Either] ^ <console>:14: error: kinds of the type arguments (<error>) do not conform to the expected kinds of the type parameters (type F). <error>'s type parameters do not match type F's expected parameters: <none> has no type parameters, but type F has one "oppai".pure[Either]
pure[Either]がエラーになっているのは、"error: Either takes two type parameters, expected: one"というエラーメッセージが示すとおり、pureはkindが「* -> *」である型を期待しているにも関わらず、「* -> * -> *」である型が渡されたからだと分かります。
ここで、pureを利用して、LeftがStringであるEitherに値を包みたいとします。どう書けばいいでしょうか?
scala> 1.point[Either[String,_]] <console>:17: error: Either[String, _] takes no type parameters, expected: one 1.point[Either[String,_]] scala> 1.point[Either[String,Int]] <console>:17: error: Either[String,Int] takes no type parameters, expected: one 1.point[Either[String,Int]
pureには、kindが「* -> *」である型コンストラクタを渡す必要があります。pureからLeftがStringでRightが任意に変わるような、「* -> *」な型コンストラクタが必要です。このようにすれば解決します。
scala> type StringEither[A] = Either[String,A] defined type alias StringEither scala> 1.pure[StringEither] res42: StringEither[Int] = Right(1)
typeを利用して、型パラメータを一つ取って、Either[String, A]を返す型コンストラクタ(StringEither[_])を定義しました。このStringEitherのkindは「* -> *」なので、pureに渡すことができます。解決です。
ですが、いちいちtypeで型を定義してからpureを呼ぶ必要があるのは面倒です。Listのmapやfilterのような高階関数を呼び出すときに、いちいち関数を別な変数に束縛してから呼び出す、なんてことはふつうはしないですよね?
型コンストラクタでも、無名関数のように「その場で使い捨てる型コンストラクタ」を定義できれば便利です。
[({type F[X] = G[A,X]})#F] は、まさしくそのためのイディオムなのです。なんだってーーー!!!!!
scala> 1.pure[({type F[A] = Either[String,A]})#F] res44: scala.util.Either[String,Int] = Right(1)
pureには({type F[A] = Either[String,A]})#F、という無名型コンストラクタ(造語です。本当にそう呼ぶかはわかりません)を渡してます。この無名型コンストラクタでは、型パラメータを一つとってEither[String, A]を返す型婚ストラクタFを動的に定義して、そのFがpureに渡されているので、kindも一致して問題なくpureが動作します。
応用編
さて、このような動的な型コンストラクタの定義を、関数が型パラメータを受け取る際に行うことがでます。
例として、任意の型A,Bを受け取って、pureを利用してEitherに包み込む関数pureEitherを定義します。
scala> def pureEither[A,B](v:B) = v.point[({type F[X] = Either[A,X]})#F] pureEither: [A, B](v: B)scala.util.Either[A,B]
このpureEitherでは、型パラメータAとBから、EitherにAを部分適用した無名型コンストラクタFをpureに渡すことで、A, Bからpureを利用してEitherに包むことを実県しています。
scala> pureEither[String,Int](1) res49: scala.util.Either[String,Int] = Right(1) scala> pureEither[Throwable,Boolean](false) res50: scala.util.Either[Throwable,Boolean] = Right(false)|scala|
以上。