1. 概要

Arrowは、KΛTEGORYfunKTionaleを統合したライブラリです。

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

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

2. Mavenの依存関係

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

<dependency>
    <groupId>io.arrow-kt</groupId>
    <artifactId>arrow-core</artifactId>
    <version>0.7.3</version>
</dependency>

3. 機能データ型

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

3.1. モナド入門

ここで説明するデータ型のいくつかはモナドです。 非常に基本的に、モナドには次のプロパティがあります。

  • これらは、基本的に1つ以上の生の値のラッパーである特別なデータ型です。
  • それらには3つのパブリックメソッドがあります。
    • 値をラップするファクトリメソッド
    • マップ
    • flatMap
  • これらのメソッドはうまく機能します。つまり、副作用はありません。

Javaの世界では、配列とストリームはモナドですが、オプションはではありません。 モナドの詳細については、ピーナッツの袋が役立つかもしれません。

次に、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 は、JavaのOptionalと同様に、存在しない可能性のある値をモデル化するためのデータ型です。

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

2つのタイプを含めることができます。値を囲む一部のラッパー、または値がない場合はNoneです。

オプションを作成する方法はいくつかあります。

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

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

ここで注意が必要な点があります。これは、ファクトリメソッドとコンストラクタがnullに対して異なる動作をすることです。

val constructor : Option<String?> = Option(null)
val fromNullable : Option<String?> = 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. どちらか

前に見たように、 Option は、値なし( None )または何らかの値( Some のいずれかになります。

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

val rightOnly : Either<String,Int> = Either.right(42)
val leftOnly : Either<String,Int> = Either.left("foo")

このクラスは、右バイアスになるように設計されています。 したがって、適切なブランチには、ビジネス価値、たとえば、何らかの計算の結果が含まれている必要があります。 左側のブランチは、エラーメッセージまたは例外さえも保持できます。

したがって、値抽出メソッド( 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 は、操作の評価を制御するために設計されたモナドです。 メモ化と熱心で遅延評価のサポートが組み込まれています。

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メソッドが呼び出された後にのみ変更されます。

後のファクトリメソッドは、関数から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番目のファクトリは常にです。 Eval インスタンスを作成し、valueが呼び出されるたびに指定された関数を再計算します。

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. オプションによるエラー処理

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

幸い、Kotlinには便利で例外的に安全な方法があります。

fun parseInput(s : String) : Option<Int> = 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<Boolean> {
    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. どちらかでのエラー処理

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

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

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

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

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

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

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

これは、どちらか番号またはエラーオブジェクトのいずれかが入力されることを意味します。

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

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

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

これで、クライアントは入力の何が問題だったかを正確に知ることができます。

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の機能機能をサポートするために作成されました。 矢印コアパッケージで提供されているデータ型を調査しました。 次に、機能スタイルのエラー処理にオプションどちらかを使用しました。

いつものように、コードはGitHubから入手できます。