ARROWを触ってみたいけど、周りにARROWを触っている人が居ない…とかそんな(どんな)人向け
結論
公式DocumentのBeginnerタグ(?)ついているのを読むとか。
あとは試しに使ってみて…大丈夫ほら、一回だけなら…
ってことで、最初にこの辺知ってたら楽しいかも?みたいなのを雑に紹介。
Option
kotlinのNullSafety(というかNullableというか)は便利だけどなぁ…
というあなたのために。
「NullSafety(というかNullableというか)じゃだめなの??」という人は「Nullable Option」とかでググると色々な記事が出てくると思いますので、
なるほど!!ってなった場合に特におすすめです(ここでのチャーンレート高そうだ)
アプローチが違うというか…でもほら、一回使ってみるだけなら…
fun <A> convertSample(src:A?) {
val a:Option<A> = Option.fromNullable(src)
val b:Option<A> = src.toOption()
}
fun <A> convertSampleFromNonNull(src:A) {
convertSample(src)
val a:Option<A> = Option(src)
val b:Option<A> = Option.just(src)
val c:Option<A> = src.some()
}
といろいろとありますが、まあtoOption()で統一しておけば良いんじゃないでしょうか?
チョット特殊例?
FunctionalJavaでOption#iifとか使っていた人向け
いやまあそんな人がこのブログ見て得られるものってあるのかというのは置いといて…
fun condition(...):Boolean = ...
fun returnValue():A
val a:Option<A> = condition(...).maybe{ returnValue() }
Booleanにmaybeという拡張関数が生えているで、そちらで
例
fun <A, B, C> nonErrorMethod(a: A, b: B, c: C): Result {
// エラーも副作用もないような処理
}
fun <A, B, C> kotlinSample(a: A?, b: B?, c: C?) {
val result1: Result? = if (a != null && b != null && c != null) {
nonErrorMethod(a, b, c)
} else {
null
}
val result2: Result? = a?.let { nonNullA ->
b?.let { nonNullB ->
c?.let { nonNullC ->
nonErrorMethod(nonNullA, nonNullB, nonNullC)
}
}
}
}
fun <A, B, C> useArrowSample(a: A?, b: B?, c: C?) {
val optionA = a.toOption()
val optionB = b.toOption()
val optionC = c.toOption()
val result1:Option<Result> = optionA.flatMap { nonNullA ->
optionB.flatMap { nonNullB ->
optionC.map{ nonNullC ->
nonErrorMethod(nonNullA, nonNullB, nonNullC)
}
}
}
val result2: Option<Result> = Option.monad().binding {
val a:A = optionA.bind()
val b:B = optionB.bind()
val c:C = optionC.bind()
nonErrorMethod(a, b, c)
}.fix()
val result3: Option<Result> = Option.applicative()
.map(optionA, optionB, optionC) { (a, b, c) -> nonErrorMethod(a, b, c) }.fix()
}
多分だいたいこんな感じでしょう。
何れの場合も引数の何れかがNull/Noneの場合、結果もNull/Noneになりますね。
monadとかapplicativeとか出てきていますが、
一旦は↑のサンプルみたいな挙動をするようになる便利な構文…で良いと思います。
特に前者はScala使っている人とかにはfor式チックなやつ…とかで…
具体的には
Option.monad().binding{...}.fix()でくくると、
... の中でOption<X>に対して.bind()ってメソッドが生える
呼ぶとOptionがNoneだったら処理を抜けてNoneを返し
SomeだったらXを返して次へ進む
Option.applicative()(aOption,bOption,cOption,..., {(a,b,c,...) -> ... }).fix()
で、aOption,bOptino,cOption,... の全部がSomeだった場合だけ、最後の引数で渡したメソッドが実行される
みたいな。
怖い人に怒られそうだけど、そんな感じで良いと思います。
reference
下記にもっと書いてある
https://arrow-kt.io/docs/datatypes/option/
Either
個人的な印象なのですが、LiveDataとかObservable各種を使っていると、
この手のものを自前で実装する(したくなる)ケースが多いと思います。
成功時だけじゃなくて失敗時も伝搬させたい…みたいな
そんなあなたのために、ちゃんと用意されています。
例
sealed class MyError(val msg:String)
object MyError1 : MyError("error 1")
object MyError2 : MyError("error 2")
val liveData:LiveData<Either<MyError, Result>> = MutableLiveData()
liveData.observe(..., Observer{ either ->
handlingSample(either)
})
fun handlingSample(either: Either<MyError, Result>) {
val result1: String = either.fold(
{
error ->
error.msg
},
{
res ->
"success"
}
)
val result2: String = when (either) {
is Either.Left -> when (either.a) {
MyError1 -> "MyError1 ${either.a}"
MyError2 -> "MyError2 ${either.a}"
}
is Either.Right -> "success"
}
}
fun aEither():Either<MyError, A>
fun bEither():Either<MyError, B>
fun cEither():Either<MyError, C>
fun useArrowSample() {
val result1 = aEither().flatMap { a ->
bEither().flatMap { b ->
cEither().map { c ->
nonErrorMethod(a, b, c)
}
}
}
val result2: Either<MyError, Result> = Either.monad<MyError>().binding {
val a = aEither().bind()
val b = bEither().bind()
val c = cEither().bind()
nonErrorMethod(a, b, c)
}.fix()
val result3: Either<MyError, Result> =
Either.applicative<MyError>().map(aEither(), bEither(), cEither()) { (a, b, c) -> nonErrorMethod(a, b, c) }.fix()
}
上の方はLiveDataでエラーのときもなんかしたい(この場合タダStringにまるめているだけですが…)とかの雑なサンプルを
後半部分は、なんかOptionの時とよく似てますね?
これ素直なKotlinで書くってなるとどう書くんでしょうか…
LiveDataとかで伝搬させるためのクラスを作る感じになると思うので、GoogleSamplesとかだとこの辺とか…?
reference
下記にもっと色々と書いてある
https://arrow-kt.io/docs/datatypes/either/
Try
似たようなので(?)1.3-M1でSuccessOrFailureとかあるけどなんかこれ…
we discourage using SuccessOrFailure as return type of functions
ってことらしいし
この方のコメントで言われているぐらいに捉えたほうが納得するというか?
This SuccessOrFailure class SHOULD NOT be used directly as a return type only in case the failure is handled by the caller locally. Where nullable type and sealed class hierarchy could be used instead.
そうすればこの辺の構文がわからんでもないというか...
いやまあ使ってわかりやすいのかこれ…
まあ1.3でたらわかるでしょうし、長いものには巻かれます。
ちなみにM2で触れられているContractsのほうが気になっていますし、欲しい。
Contracts構文素敵じゃないですか?
Internalでは1.1か2くらいから使われていたんだし、はよ出してくれ。
例
多分もう想像つくかと
sealed class MyError(val msg:String): Throwable(msg)
object MyError1 : MyError("error 1")
object MyError2 : MyError("error 2")
val liveData:LiveData<Try<Result>> = MutableLiveData()
liveData.observe(..., Observer{ _try ->
handlingSample(_try)
})
fun handlingSample(src: Try<Result>) {
val result: String = src.fold(
{ error ->
when (error) {
is MyError1 -> error.msg
is MyError2 -> error.msg
else -> "unknown error"
}
},
{ res ->
"success"
}
)
}
fun aSometimeThrow():A
fun bSometimeThrow():B
fun cSometimeThrow():C
fun aTry():Try<A> = Try{ aSometimeThrow() }
fun bTry():Try<B> = Try{ bSometimeThrow() }
fun cTry():Try<C> = Try{ cSometimeThrow() }
fun kotlinSample() {
try {
val a = aSometimeThrow()
val b = bSometimeThrow()
val c = cSometimeThrow()
nonErrorMethod(a, b, c)
} catch (e:MyError) {
// ...
} catch (e:Throwable) {
// ...
}
}
fun useArrowSample() {
val result1:Try<Result> = aTry().flatMap { a ->
bTry().flatMap { b ->
cTry().map { c ->
nonErrorMethod(a, b, c)
}
}
}
val result2: Try<Result> = Try.monad().binding {
val a = aTry().bind()
val b = bTry().bind()
val c = cTry().bind()
nonErrorMethod(a, b, c)
}.fix()
val result3: Try<Result> =
Try.applicative().map(aTry(), bTry(), cTry()) { (a, b, c) -> nonErrorMethod(a, b, c) }.fix()
// call handlineSample etc...
}
なんていうかびっくりするくらいOption版のコピペですね!
reference
Tryに関しては結構色々なHandling方法や、Eitherへの変換とかあるので見ておいて損ないです
Either<Throwable,...>
に便利メソッド生やしたって読み替えても良いと思います(雑)
https://arrow-kt.io/docs/datatypes/try/
中間まとめ
ところどころドキュメント古い
正直この記事書く意味有るのかわからないくらい、公式Document充実しています。
が、注意する場所も…
Kotlin Slackの#arrowで
we're deprecating ForXX extensions in favor of generating the extension functions directly on the types
と言っていますので、
ForOption extensions {
binding {
...
}
}
を
Option.monad().binding {
...
}
とかと読み替えておく必要はありそうです。
他
Option/Either/Tryの利用例が似ているというか一緒ですよね。
ということは、実は大体他に用意されているやつもおんなじように使えるようになっています。
ただのInterfaceっぽいですよね。
実はそれがM的なあれに…みたいなこと書くとアチラコチラから刺されそうですが…
ここの説明とか読むとなんか納得してもらえるんじゃないかな
とはいえここ見ている人は多分英語辛い派閥でしょうから、
上のリンクの「サンプルコードだけをゆっくり読めば」なるほど感になるのでは??
疲れたので雑にいくつか
途中Eitherとかで包んでないんだけどよしなに包んで欲しい
val res1 = Either.monadError<Throwable>().bindingCatch {
val a = Either.right(1).bind()
val b = throwError() // ここでLeftになって返る
a + b
}.fix()
val res2 = Try.monadError().bindingCatch {
val a = Try.just(1).bind()
val b = throwError() // ここでFailureになって返る
a + b
}.fix()
monad().binding{...}
→ monadError().bindingCatch{...}
に置き換えるとあら不思議
ただし、何でもかんでもbindingCatchが呼べるわけじゃないので…
IDEの補完で気がつくと思いますのでそれで。
Eitherの場合、LeftがThrowableなものに限定されます
rxjavaチックにThread切り替えたい
Single.just(...)
.map(...)
.observeOn(Schedulers...)
...
.subscribeOn(AndroidSchedulers...)
...
的なやつ
根本的にタダの非同期処理として使う場合での比較という点はお間違えなく。
ちなみにメソッドチェーンで繋がなくてもできます
val (res1, disposable1) = IO.async().run {
bindingCancellable {
continueOn(Unconfined)
val a = IO.just(1).bind()
val b = throwError()
a + b
}
}
res1.fix().attempt().unsafeRunSync().fold(
{
},
{
fail()
}
)
val (res2, disposable2) = DeferredK.async().run {
bindingCancellable {
continueOn(Unconfined)
val a = DeferredK.just(1).bind()
val b = throwError()
a + b
}
}
res2.fix().unsafeRunAsync {
it.fold(
{
},
{
fail()
}
)
}
こんな感じとか。
さっきのbindingCatchの更に上位版みたいな(?)やつでCancellableというのがあります。
こうすると、戻り値がTupleになって、戻り値の2個めがCancel用のものになっています。
書き方が色々とあるのは良いのか悪いのか…
上の例だと disposable1()
とかでCancelされます
List<Option<Int>>
みたいなのが、Option<List<Int>>
にならないかな?
これ、SuccessOrFailureの記事でもList<SuccessOrFailure<String>>
みたいなの出てきてて思い出しました。
SuccessOrFailure<List<String>>
みたいにしたい!とかありそうな気がするんですけども…
(まあError複数ある場合の処理を期待しているときはあれだが)
最後にReturnされるものがList<Option<Int>>
とかだったら中身精査しろよ感ありますが、
処理の途中でOption<...>
だったらflatMapに食わせられるのに…とかそんな時用途でしょうか。
val src = listOf(1, 2, 3, 4)
val target:List<Option<Int>> = src.map { (it % 2 == 0).maybe { it } }
val actual:Option<List<Int>> = target.k().sequence(Option.applicative()).fix()
assertEquals(actual, None)
val target2:List<Try<Int>> = src.map {
Try {
if (it % 2 == 0) it
else throw Exception("$it")
}
}
val actual2:Try<List<Int>> = target2.k().sequence(Try.applicative()).fix()
actual2.fold(
{
assertTrue(it.message == "1")
},
{
fail()
})
上の例だとそもそも奇数フィルターしろや案件ですがそのへんはまあ…例なので…
sequence
とapplicative
というワードを覚えておくと良いことあるかもしれません。
これ、公式Documentだとどこなんでしょう…
Traverseあたりにそのうち書かれるのかな?
https://arrow-kt.io/docs/typeclasses/traverse/#traverse
もっと強力な使い方無いの??
Option<Either<X,Y>>
みたいなのをキレイに(ネスト無く)使いたい!
とかになってくると、
OptionTとか
EitherTとか知りたくなるかと思います。
あとAndroidで…という文脈では余り使う機会が想像できないけど、
意味がわかるとなるほど!!って思えるのがValidatedとEitherを切り替えるサンプルかと思います。
このサンプルだと、FormのFieldの入力をラベル、アドレス両方まとめてチェックしますが、
多分Android(というかフロントというか)だと、入力フォームそのものに(入力した端から)Validationをかけて、そのまま最初のErrorだけ表示
で、そのValidation(たち)を通ったものだけが伝搬してくるようにするかと思うので…
なんかあえてこっちを(特にValidatedを)使うケースがぱっと想像つかないんですよね。(あくまでもAndroid脳だと?)
なおこの辺がサンプルとしてあります。
ただ、このサンプルから学べることはいっぱいあると思うのでおすすめです。
まあ入れたらフル機能使わなければならない!なんてルールは無いですし、
Option/Eitherあたりから始めて、良かったら他のも試してみればよいかと思います。
終わり
なんか間違っていたり、もっとここんとこ書いて欲しい!とかありましたら、雑にご連絡ください。
.k()
とかKind<F,A>
とかの説明とかが喜ばれたりするんでしょうか???
気が向いたら書きます。
Top comments (1)
ありがとう🦄