技術ブログを開設しました。

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

こんにちは。GBS第2システム開発部の小路です。

関数型プログラミングの手助けをするKotlin製のライブラリにArrowというものがありますが、それを使うためには、基本的な用語を知っている必要があります。
今回は主にArrowに記載の内容を元に、型コンストラクタと高カインド型の概要をまとめます。

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

  • 型コンストラクタとは少なくとも1つのジェネリックパラメータを持つクラス(インタフェース)である(例えば、 ListK<A>、Option<A>(※1)のAがジェネリックパラメータに相当)
  • ListK<A>のAに対して、Int型を割り当てた場合、ListKはListK<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>のみであるため、ForOptionとOption<A>は密接な結びつきを持つ。そのため、後述のKind<ForOption, A>.fix()を呼び出すことで、Kind<ForOption, A>からOptionへ安全にダウンキャストができる。

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

Kind<F, A>のFに対して、Arrowで提供される全てのDatatypes(OptionやListK、Eitherなど)に適用できる、fixという関数が実装されています。
これにより、KindクラスからOptionやEitherなどのサブクラスにダウンキャストができるようになっています。

例えば、Kind<F, A>のFにForOptionが適用された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も同様)であるため、ダウンキャストが安全に行えるようになっています。