読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

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|

以上。