Posted at

Arrowを使ってAndroidで関数型プログラミング〜型コンストラクタ、高カインド型について〜

関数型プログラミングの手助けをするKotlin製のライブラリであるArrowを使うためには、基本的な用語を知っている必要があります。

今回は主にArrowに記載の内容を元に、型コンストラクタと高カインド型の概要をまとめます。


Arrowにおける型コンストラクタについて



  • 型コンストラクタとは少なくとも1つのジェネリックパラメータを持つクラス(インタフェース)である(例えば、 ListK<A>Option<A>(※1)のAがジェネリックパラメータに相当)


  • ListK<A>Aに対して、Int型を割り当てた場合、ListKListK<Int>となる。このとき、ListKにはジェネリックパラメータではなく、Intと言う具体的な型が指定されているため、型コンストラクタとは見なされなくなり、通常の「型」と見なされるようになる。

  • 複数のパラメータを持つ型コンストラクタ(例:Either<L, R>)の一つのパラメータに対して型を適用した場合、その結果まだもう一方のジェネリックパラメータが残っているので、これは型コンストラクタと見なされる。(例えば、Either<L, R>Lに対して、Throwableを適用すると、Either<Throwable, R>となるが、これはRというジェネリックパラメータを持っているので、型コンストラクタと見なされる。)

  • Kotlinでは型コンストラクタはファーストクラスとして扱われていないので、ArrowではKind<F, A>というインタフェースを利用して、それを表現している。

※1… ListK, OptionのいずれもArrowが提供するAPI。ListKはKotlin標準のListクラスをラップして、モナド則を満たす関数を提供する。


Arrowにおける高カインド型とは


  • カインドをざっくり言い表すと、「型コンストラクタ」の「型」のことである。

  • 型パラメータを取らない型(Int、Stringなど)は*で表現され(零項の型コンストラクタと言う事もある)、Option<A>などの一つのパラメータを取る型コンストラクタ(1階の型コンストラクタと言う)は* -> *と表現される。

  • n階の型コンストラクタは、型パラメータに具体型を適用することで普通型となる。上述の例の通り、 ListK<A>は1階の型コンストラクタだが、このAに対して、Int型を割り当てた場合、ListK<Int>となり、普通型となる。

  • Arrowでは1階の型コンストラクタはKind<F, A>というインタフェースで表現され、Fはコンテンツをラップするコンテナの型を表し、Aはコンテンツの型を表す。(2階の型コンストラクタは(* -> *) -> *で表現され、ArrowではKind<Kind<F, A>, B>で表す。3階以降の表現方法も同様。)


  • Kind<F, A>Fを表現するための代理クラスがあり、その代理クラス名はコンテナのクラス名に対して接頭辞「For」を付与したものである。(例:ForListK、ForOption)

具体的に、OptionクラスとForOptionクラスの関係性は以下のようになります。

class ForOption private constructor() { companion object {} }

typealias OptionOf<A> = arrow.Kind<ForOption, A>

@higherkind
sealed class Option<out A> : OptionOf<A>
// => sealed class Option<out A>: Kind<ForOption, A>

上記の通り、OptionクラスはKind<ForOption, A>を実装するクラスとして定義されており、以下を満たすことが読み取れます。



  • Option<out A>はコンテンツの型として、A(ジェネリックパラメータ)をもつ。


  • Option<out A>はコンテナの型(コンテンツをラップする役割を持つ型)としてForOptionをもつ。(※2)


  • Option<out A>はパラメータAを持ち、カインドとして* -> *(「1階の型コンストラクタ」)で表現できる。


  • Option<out A>のAに対して、具体的な型- 例えばInt -を適用すると、Option<Int>となり、型コンストラクタではなく具体的な型(カインドは「*」)となる。

※2… Kind<ForOption, A>を実装するのはOption<A>のみであるため、ForOptionOption<A>は密接な結びつきを持つ。そのため、後述のKind<ForOption, A>.fix()を呼び出すことで、Kind<ForOption, A>からOptionへ安全にダウンキャストができる。


Kind<F, A>の拡張関数について

Kind<F, A>Fに対して、Arrowで提供される全てのDatatypes(OptionやListK、Eitherなど)に適用できる、fixという関数が実装されています。

これにより、KindクラスからOptionEitherなどのサブクラスにダウンキャストができるようになっています。

例えば、Kind<F, A>FForOptionが適用されたfix関数は以下の通りです。


inline fun <A> OptionOf<A>.fix(): Option<A> = this as Option<A>

// => inline fun <A> Kind<ForOption, A>.fix(): Option<A> = this as Option<A>


Arrowライブラリの実装上、Kind<ForOption, A>を実装するのはOption<A>のみ(他のDatatype(ListK, Either, etcも同様)であるため、ダウンキャストが安全に行えるようになっています。