コトリンの矢入門
1概要
Arrowはhttp://kategory.io/[KΛTEGORY]とhttps://github.com/MarioAriasC/funKTionale[funKTionale]からマージされたライブラリです。
このチュートリアルでは、Arrowの基本と、それがどのようにしてKotlinの関数型プログラミングの力を利用するのに役立つのかを見ていきます。
コアパッケージのデータ型について説明し、エラー処理に関するユースケースを調査します。
2. Mavenの依存関係
プロジェクトにArrowを含めるには、https://search.maven.org/search?q=g:io.arrow-kt%20AND%20a:arrow-coreを追加する必要があります。
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.7.3</version>
</dependency>
3機能データ型
コアモジュールのデータ型を調べることから始めましょう。
3.1. モナド入門
ここで説明するデータ型のいくつかはモナドです。非常に基本的に、モナドには以下の性質があります。
-
それらは基本的に1つの周りのラッパーである特別なデータ型です
より生の値
** 3つの公開メソッドがあります。
-
** 値をラップするファクトリメソッド
-
**
map
-
**
flatMap
-
これらのメソッドは
nicely
、つまり副作用がありません。
Javaの世界では、配列とストリームはモナドですが、https://www.sitepoint.com/how-optional-breaks-the-monad-laws-and-why-it-matters/[オプションではありません]。モナドの詳細についてはhttps://medium.com/beingprofessional/understanding-functor-and-monad-with-a-bag-of-peanuts-8fa702b3f69e[ピーナッツの袋]が役立つことがあります]。
それでは、
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つのタイプを含めることができます。値を囲む
_Some
ラッパー、または値がない場合は
None_
。
Option
を作成する方法はいくつかあります。
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
__riskを持っていないので、私たちは2番目を好みます:**
try {
constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
fromNullable.map { s -> s!!.length }
}
3.3.
Either
前に見たように、
Option
には値を持たない(
None
)か、または何らかの値(
Some
)を付けることができます。
Either
はこのパスをさらに進み、2つの値のうちの1つを持つことができます。
Either
には、
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
factoryメソッドを使うと、すでに計算された値から
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
__methodが呼び出された後にのみ変更されることがわかります。
__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<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
を使ったエラー処理
Either
でエラーケースに関する情報を返すためのいくつかのオプションがあります。左側には、文字列メッセージ、エラーコード、さらには例外を含めることができます。
今のところ、この目的のためにシールされたクラスを作成します。
sealed class ComputeProblem {
object OddNumber : ComputeProblem()
object NotANumber : ComputeProblem()
}
返された
Either
にこのクラスを含めます。 parseメソッドでは
cond
ファクトリ関数を使います。
Either.cond(/Condition/,/Right-side provider/,/Left-side provider/)
そのため、
__Optionではなく、
Ether
を
parseInput__メソッドで使用します。
fun parseInput(s : String) : Either<ComputeProblem, Int> =
Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )
これは
Either
が
__数字かエラーオブジェクトのどちらかで埋められることを意味します。
他のすべての機能は以前と同じになります。ただし、
filter
メソッドは
Either
とは異なります。それは述語だけでなく、述語の偽分岐のための左側のプロバイダを必要とします。
fun computeWithEither(input : String) : Either<ComputeProblem, Boolean> {
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パッケージで提供されているデータ型を調べました。
それから
Optional
と
Either
を関数型のエラー処理に使いました。
いつものように、コードはhttps://github.com/eugenp/tutorials/tree/master/kotlin-libraries[over GitHub]から入手可能です。