コトリンの矢入門

KotlinのArrowの概要

1. 概要

Arrowは、KΛTEGORYfunKTionaleからマージされたライブラリです。

このチュートリアルでは、Arrowの基本と、Kotlinの関数型プログラミングの力を活用するのにどのように役立つかを見ていきます。

コアパッケージのデータ型について説明し、エラー処理に関するユースケースを調査します。

2. メーベン依存

プロジェクトにArrowを含めるには、the arrow-core dependencyを追加する必要があります。


    io.arrow-kt
    arrow-core
    0.7.3

3. 機能データ型

コアモジュールのデータ型を調査することから始めましょう。

3.1. モナド入門

ここで説明したデータ型の一部はモナドです。 非常に基本的に、モナドには次のプロパティがあります。

  • これらは、基本的に1つ以上の生の値のラッパーである特別なデータ型です

  • これらには3つのパブリックメソッドがあります。

    • 値をラップするファクトリーメソッド

    • map

    • flatMap

  • これらのメソッドはnicelyを実行します。つまり、副作用はありません。

Javaの世界では、配列とストリームはモナドですが、Optional isn’tです。 モナドの詳細については、おそらくbag of peanuts can helpです。

次に、arrow-coreモジュールの最初のデータ型を見てみましょう。

3.2. Id

Idは、Arrowで最も単純なラッパーです。

コンストラクターまたはファクトリーメソッドを使用して作成できます。

val id = Id("foo")
val justId = Id.just("foo");

また、ラップされた値を取得するためのextractメソッドがあります。

Assert.assertEquals("foo", id.extract())
Assert.assertEquals(justId, id)

Idクラスは、モナドパターンの要件を満たしています。

3.3. Option

Optionは、Javaのオプションと同様に、存在しない可能性のある値をモデル化するためのデータ型です。

技術的にはモナドではありませんが、それでも非常に役立ちます。

2つのタイプを含めることができます。値を囲むSome wrapper、または値がない場合はNoneです。

Optionを作成する方法はいくつかあります。

val factory = Option.just(42)
val constructor = Option(42)
val emptyOptional = Option.empty()
val fromNullable = Option.fromNullable(null)

Assert.assertEquals(42, factory.getOrElse { -1 })
Assert.assertEquals(factory, constructor)
Assert.assertEquals(emptyOptional, fromNullable)

Now, there is a tricky bit here,は、ファクトリメソッドとコンストラクタがnullに対して異なる動作をすることです。

val constructor : Option = Option(null)
val fromNullable : Option = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)

KotlinNullPointerException リスクがないため、2番目を優先します。

try {
    constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
    fromNullable.map { s -> s!!.length }
}

3.3. Either

前に見たように、Optionは値がない(None)か、ある値(Some.のいずれかです。

Eitherはこのパスをさらに進み、2つの値のいずれかを持つことができます。 Eitherには、rightおよびleft:として示される2つの値のタイプの2つの汎用パラメーターがあります。

val rightOnly : Either = Either.right(42)
val leftOnly : Either = Either.left("foo")

このクラスはright-biasedになるように設計されています。 そのため、右のブランチにはビジネス上の価値、たとえば何らかの計算の結果を含める必要があります。 左側のブランチには、エラーメッセージや例外を保持できます。

したがって、値抽出メソッド(getOrElse)は、右側に向かって設計されています。

Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })

mapメソッドとflatMapメソッドでさえ、右側で機能し、左側をスキップするように設計されています。

Assert.assertEquals(0, rightOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertTrue(rightOnly.flatMap { Either.Right(it % 2) }.isRight())
Assert.assertTrue(leftOnly.flatMap { Either.Right(it % 2) }.isLeft())

セクション4で、エラー処理にEitherを使用する方法を調査します。

3.4. Eval

Evalは、操作の評価を制御するために設計されたモナドです。 It has a built-in support for memoization and eager and lazy evaluation.

nowファクトリメソッドを使用すると、すでに計算された値からEvalインスタンスを作成できます。

val now = Eval.now(1)

mapおよびflatMap操作は遅延して実行されます。

var counter : Int = 0
val map = now.map { x -> counter++; x+1 }
Assert.assertEquals(0, counter)

val extract = map.value()
Assert.assertEquals(2, extract)
Assert.assertEquals(1, counter)

ご覧のとおり、counterは、value メソッドが呼び出された後にのみ変更されます。

later factoryメソッドは、関数からEvalインスタンスを作成します。 評価はvalueの呼び出しまで延期され、結果はメモ化されます。

var counter : Int = 0
val later = Eval.later { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(1, secondValue)
Assert.assertEquals(1, counter)

3番目のファクトリはalwaysです。 valueが呼び出されるたびに、指定された関数を再計算するEvalインスタンスを作成します。

var counter : Int = 0
val later = Eval.always { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(2, secondValue)
Assert.assertEquals(2, counter)

4. 機能的なデータ型のエラー処理パターン

例外をスローすることによるエラー処理には、いくつかの欠点があります。

ユーザー入力を数値として解析するなど、頻繁かつ予測どおりに失敗するメソッドの場合、例外をスローするのはコストがかかり、不要です。 コストの大部分はfillInStackTraceメソッドから発生します。 実際、最新のフレームワークでは、ビジネスロジックに関する情報が驚くほど少ないため、スタックトレースが途方もなく長くなる可能性があります。

さらに、チェックされた例外を処理すると、クライアントのコードが不必要に複雑になる可能性があります。 一方、ランタイム例外では、呼び出し元には例外の可能性に関する情報がありません。

次に、偶数の入力数の最大除数が平方数であるかどうかを確認するソリューションを実装します。 ユーザー入力は文字列として届きます。 この例とともに、Arrowのデータ型がエラー処理にどのように役立つかを調査します

4.1. Optionによるエラー処理

まず、入力文字列を整数として解析します。

幸いなことに、Kotlinには便利で例外安全なメソッドがあります。

fun parseInput(s : String) : Option = Option.fromNullable(s.toIntOrNull())

解析結果をOptionにラップします。 次に、この初期値をいくつかのカスタムロジックで変換します。

fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...

Optionの設計のおかげで、例外処理やif-elseブランチでビジネスロジックが乱雑になることはありません。

fun computeWithOption(input : String) : Option {
    return parseInput(input)
      .filter(::isEven)
      .map(::biggestDivisor)
      .map(::isSquareNumber)
}

ご覧のとおり、技術的な詳細の負担がない純粋なビジネスコードです。

クライアントが結果をどのように処理できるかを見てみましょう。

fun computeWithOptionClient(input : String) : String {
    val computeOption = computeWithOption(input)
    return when(computeOption) {
        is None -> "Not an even number!"
        is Some -> "The greatest divisor is square number: ${computeOption.t}"
    }
}

これは素晴らしいことですが、クライアントは入力の何が悪かったのかについての詳細な情報を持っていません。

それでは、Eitherを使用してエラーケースのより詳細な説明を提供する方法を見てみましょう。

4.2 Error Handling with Either

Eitherでエラーケースに関する情報を返すためのいくつかのオプションがあります。 左側には、文字列メッセージ、エラーコード、または例外を含めることができます。

今のところ、この目的のために封印されたクラスを作成します。

sealed class ComputeProblem {
    object OddNumber : ComputeProblem()
    object NotANumber : ComputeProblem()
}

返されるEitherにこのクラスを含めます。 解析メソッドでは、condファクトリ関数を使用します。

Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)

したがって、Option, の代わりに、parseInputメソッドでEither を使用します。

fun parseInput(s : String) : Either =
  Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )

これは、Eithereither 番号またはエラーオブジェクトが入力されることを意味します。

他のすべての機能は以前と同じです。 ただし、filterメソッドはEitherでは異なります。 述語だけでなく、述語のfalseブランチの左側のプロバイダーも必要です。

fun computeWithEither(input : String) : Either {
    return parseInput(input)
      .filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
      .map (::biggestDivisor)
      .map (::isSquareNumber)
}

これは、フィルターがfalseを返す場合に備えて、Eitherの反対側を指定する必要があるためです。

これで、クライアントは入力で何が間違っていたかを正確に知ることができます。

fun computeWithEitherClient(input : String) {
    val computeWithEither = computeWithEither(input)
    when(computeWithEither) {
        is Either.Right -> "The greatest divisor is square number: ${computeWithEither.b}"
        is Either.Left -> when(computeWithEither.a) {
            is ComputeProblem.NotANumber -> "Wrong input! Not a number!"
            is ComputeProblem.OddNumber -> "It is an odd number!"
        }
    }
}

5. 結論

Arrowライブラリーは、Kotlinの機能をサポートするために作成されました。 arrow-coreパッケージで提供されているデータ型を調査しました。 次に、関数型のエラー処理にOptionalEitherを使用しました。

いつものように、コードはover on GitHubで利用できます。