Kotlin の関数型プログラミング用ライブラリ Λrrow を試してみる
Kotlin で Scala の Scalaz や Cats のような関数型プログラミング用のライブラリを探していたところ、以下を見つけたので試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180822/
はじめに
Λrrow は以下のような要素で構成されており、Haskell の型クラス・インスタンスのような仕組みを実現しているようです。
- Datatypes (Option, Either, Try, Kleisli, StateT, WriterT 等)
- Typeclasses (Functor, Applicative, Monad, Monoid, Eq, Show 等)
- Instances (OptionMonadInstance 等)
そして、その実現にはアノテーションプロセッサが活用されているようです。(@higherkind
や @instance
から補助的なコードを生成)
Option のソースコード
例えば、Option
のソースコードは以下のようになっていますが、OptionOf
の定義は github のソース上には見当たりません。
arrow/core/Option.kt
@higherkind sealed class Option<out A> : OptionOf<A> { ・・・ }
OptionOf はアノテーションプロセッサ ※ で生成したコード(以下)で定義されています。(OptionOf<A>
は Kind<ForOption, A>
の型エイリアス)
higherkind.arrow.core.Option.kt
package arrow.core class ForOption private constructor() { companion object } typealias OptionOf<A> = arrow.Kind<ForOption, A> @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") inline fun <A> OptionOf<A>.fix(): Option<A> = this as Option<A>
※ @higherkind に対する処理は arrow.higherkinds.HigherKindsFileGenerator に実装されているようです
ここで、ForOption
というクラスが定義されていますが、この For + 型名
はコンテナ型を表現するための型のようです。
ForOption がある事で Kind<Option<A>, A>
ではなく Kind<ForOption, A>
と定義できるようになっており、Functor<F>
や Monad<F>
における F の具体型として使われています。(例. Monad<ForOption>
)
Either のソースコード
次に Either
のソースコードも見てみます。
Either のように型パラメータが複数の場合はアノテーションプロセッサで生成されるコードの内容に多少の違いがあるようです。
arrow/core/Either.kt
@higherkind sealed class Either<out A, out B> : EitherOf<A, B> { ・・・ }
アノテーションプロセッサで生成されたコードは以下のように EitherOf
の他に EitherPartialOf
が型エイリアスとして定義されています。
higherkind.arrow.core.Either.kt
package arrow.core class ForEither private constructor() { companion object } typealias EitherOf<A, B> = arrow.Kind2<ForEither, A, B> typealias EitherPartialOf<A> = arrow.Kind<ForEither, A> @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") inline fun <A, B> EitherOf<A, B>.fix(): Either<A, B> = this as Either<A, B>
また、上記で定義されている ForEither クラスとは別に arrow-instances-core
モジュールには ForEither 関数
も用意されています。
arrow/instances/either.kt
・・・ class EitherContext<L> : EitherMonadErrorInstance<L>, EitherTraverseInstance<L>, EitherSemigroupKInstance<L> { override fun <A, B> Kind<EitherPartialOf<L>, A>.map(f: (A) -> B): Either<L, B> = fix().map(f) } class EitherContextPartiallyApplied<L> { infix fun <A> extensions(f: EitherContext<L>.() -> A): A = f(EitherContext()) } fun <L> ForEither(): EitherContextPartiallyApplied<L> = EitherContextPartiallyApplied()
この ForEither 関数は extensions
を呼び出す際に使用する事になります。
サンプルコード
Option と Either をそれぞれ使ったサンプルを作成してみます。
(a) Option の利用
Option の Some
や None
の作成にはいくつかの方法が用意されています。
Monadbinding
を使うと Haskell の do 記法や Scala の for 式のようにモナドを処理できるようです。(Kotlin の coroutines 機能で実現)
binding の呼び出し方はいくつか考えられますが、ForOption extensions { ・・・ }
を使うのが基本のようです。
ForOption extensions へ渡す処理内では this が OptionContext
となるため、OptionContext の処理を呼び出せるようになっています。
また、binding へ渡す処理内では MonadContinuation<ForOption, *>
が this となります。
bind
は MonadContinuation 内で suspend fun <B> Kind<F, B>.bind(): B
と定義されており、Kind<F, B> から B の値を取り出す処理となっています。
src/main/kotlin/App.kt
import arrow.core.* import arrow.instances.* import arrow.typeclasses.binding fun main(args: Array<String>) { // Some val d1: Option<Int> = Option.just(10) val d2: Option<Int> = 5.some() val d3: Option<Int> = Some(2) // None val d4: Option<Int> = Option.empty() val d5: Option<Int> = none() val d6: Option<Int> = None // Some(15) val r1 = d1.flatMap { a -> d2.map { b -> a + b } } println(r1) // Some(17) val r2 = Option.monad().binding { // this: MonadContinuation<ForOption, *> val a = d1.bind() // 10 val b = d2.bind() // 5 val c = d3.bind() // 2 a + b + c } println(r2) // Some(17) val r3 = ForOption extensions { // this: OptionContext println(this) // arrow.instances.OptionContext@3ffc5af1 binding { // this: MonadContinuation<ForOption, *> println(this) // arrow.typeclasses.MonadContinuation@26653222 val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } } println(r3) // Some(17) val r4 = OptionContext.binding { val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } println(r4) // None val r5 = Option.monad().binding { val a = d1.bind() val b = d4.bind() a + b } println(r5) // None val r6 = ForOption extensions { binding { val a = d5.bind() val b = d2.bind() val c = d6.bind() a + b + c } } println(r6) }
Option のような基本的な型を使うだけであれば、依存ライブラリとして arrow-instances-core
を指定するだけで問題なさそうです。
そのため、Gradle ビルド定義ファイルは下記のような内容になります。
ここでは The feature "coroutines" is experimental (see: https://kotlinlang.org/docs/diagnostics/experimental-coroutines)
警告ログを出力しないように coroutines を有効化しています。
build.gradle (Gradle ビルド定義)
plugins { id 'org.jetbrains.kotlin.jvm' version '1.2.51' id 'application' } mainClassName = 'AppKt' // 実行するクラス名 repositories { jcenter() } dependencies { compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' compile 'io.arrow-kt:arrow-instances-core:0.7.3' } // coroutines の有効化(ビルド時の警告を抑制) kotlin { experimental { coroutines 'enable' } }
実行結果は以下の通りです。
実行結果
> gradle run ・・・ Some(15) Some(17) arrow.instances.OptionContext@3ffc5af1 arrow.typeclasses.MonadContinuation@26653222 Some(17) Some(17) None None
(b) Either の利用
Either の場合、extensions の呼び出し方が Option とは多少異なります。
Option の場合は ForOption の extensions を呼び出しましたが、Either の場合は ForEither に extensions は用意されておらず、代わりに EitherContextPartiallyApplied<L>
クラス内に定義されています。
EitherContextPartiallyAppliedForEither 関数
で取得できるので、これを使って ForEither<Left 側の型>() extensions { ・・・ }
のようにします。
ForEither<String>() extensions
へ渡す処理内の this は EitherContext<String>
で、binding へ渡す処理内の this は MonadContinuation<EitherPartialOf<String>, *>
となります。
src/main/kotlin/App.kt
import arrow.core.* import arrow.instances.* import arrow.typeclasses.binding fun main(args: Array<String>) { // Right val d1: Either<String, Int> = Either.right(10) val d2: Either<String, Int> = 5.right() val d3: Either<String, Int> = Right(2) // Left val d4: Either<String, Int> = Either.left("error data") // Right(b=15) val r1 = d1.flatMap { a -> d2.map { b -> a + b } } println(r1) // Right(b=17) val r2 = Either.monad<String>().binding { // this: MonadContinuation<EitherPartialOf<String>, *> val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } println(r2) // Right(b=17) // ForEither<String>() 関数の呼び出し val r3 = ForEither<String>() extensions { // this: EitherContext<String> binding { // this: MonadContinuation<EitherPartialOf<String>, *> val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } } println(r3) // Left(a=error data) val r4 = ForEither<String>() extensions { binding { val a = d1.bind() val b = d4.bind() val c = d3.bind() a + b + c } } println(r4) }
(a) と同じ内容の build.gradle を使って実行します。
実行結果
> gradle run ・・・ Right(b=15) Right(b=17) Right(b=17) Left(a=error data)