Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

どこまでResult/Eitherを使うべきか?思考 #42

Open
8 of 12 tasks
sunakan opened this issue Jul 6, 2022 · 40 comments
Open
8 of 12 tasks

どこまでResult/Eitherを使うべきか?思考 #42

sunakan opened this issue Jul 6, 2022 · 40 comments

Comments

@sunakan
Copy link
Collaborator

sunakan commented Jul 6, 2022

Java/Kotlinで書くと、非検査例外をどこまでtry/catchするのか、むしろ全くしないほうがいいのか、全部するのか

過剰防御するのかしないのか

オリジナル?(F#)

その他資料

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

try/catchのデメリット

可読性が落ちる(いちいちチェックしてるし)

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

あー、いわゆるユースケース駆動開発本にある「雨の日」みたいなケースをResult/Eitherの左型として扱うのかな。。?

一旦「ドメインエラー」と呼ぶ ビジネス例外 準正常系

そうなってくると、全てのエラー定義にはUnexpectedはありえなくなりそう。。

それは例外としてthrowして上で必要な箇所(多分一番上)でハンドリング

  • バリデーションエラーは、ほぼドメインエラー ビジネス例外 準正常系

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

もし ドメインエラー ビジネス例外 準正常系を作成するのに、try/catchが必要ならするし、不要ならしない

検査例外・非検査例外とかで区分するのではなく、 ドメインエラー ビジネス例外 準正常系として定義するかどうか、かな?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

これがもし、例外が存在しない言語なら、Unexpectedを定義する必要があるかも

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

Rust/Goでいうpanicは not ドメインエラー ビジネス例外 準正常系 のときに利用する。。みたいな?

(Java/Kotlinだとthrow)

それをある程度仕組みで検知できそうか。。?もしかしたらinfra層だけになるかもしれない?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

Resultみたいな仕組みが無い言語において、ドメインエラー ビジネス例外 準正常系とそうじゃないエラーがどっちも Exception の仕組みを使われてるから、濫用・誤用しやすいみたいなやつかな。。?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

仮にそうだとすると、RubyとかJavaにおいて、ドメインエラー ビジネス例外 準正常系をExceptionの仕組みを利用して表現するのはBadな気がする(誤用を招きやすいため)

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

「良いコード・悪いコード」ではドメインエラー ビジネス例外 準正常系を Exceptionで表現してたけど、これがOUTなのかもしれない

いやこれをチェック例外として投げればなんとかなるかもだけど、それの使い分けが難しいなー

ドメインエラー ビジネス例外 準正常系:検査例外
それ以外:非検査例外

的な?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 6, 2022

ドメインエラーって言葉は一般的じゃなかったので、どこかで修正が必要そう

ドメインモデリングされたエラー、エラーモデル?

====

-> ビジネス例外

として記述する

言葉のチョイスは「プログラマが知るべき97のこと」

21:技術的例外とビジネス例外を明確に区別する

====

例外設計における大罪 p.29

https://www.slideshare.net/t_wada/exception-design-by-contract

-> 準正常系

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 7, 2022

Against ROP-1

Don’t use Result if you need diagnostics

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#1----dont-use-result-if-you-need-diagnostics

解釈

エラーが起きた場所などを解析したいなら、そこはResultを使うべきではない

  • Resultを例外の代用に使うな
  • ResultでStackトレースをラップするな
  • Resultで例外全てをラップするな
  • Resultはメタ情報を持つBooleanと考えろ
  • Resultはバリデーションエラー等、モデリング上で出したエラーのためのもの
  • Resultは予想されないエラーのためではない

#42 (comment)

そうなってくると、全てのエラー定義にはUnexpectedはありえなくなりそう。。

これが結果として一致しそう

(例外があればしょうがないけど、panic的なやつがあればよいか)

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-2

Don’t use Result to reinvent exceptions

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#2----dont-use-result-to-reinvent-exceptions

解釈

  • 全部Resultで包んだら、ただのtry/catchの再発明
  • Exceptionを全部覆うのは無理
    • 理由:全部覆った保証がない
      • そうすると、仕組みで解決が不可能
        • 結局最上位でcatchというかハンドリングが必要

特に後者は、そうなんだよな。。

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-3

Don’t use Result if you need to fail fast

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#3----dont-use-result-if-you-need-to-fail-fast

解釈

  • 続けられなくなった時、回復させようとしてはいけない

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-4

Don’t use Result if no one will see it

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#4----dont-use-result-if-no-one-will-see-it

解釈

  • 例外を例外として扱ってもあまり問題ないよ
  • コンパイルを通すのに必死にならなくてもいいはずだ
    • (Haskell プログラマは、それを期待するけどな)

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-5

Don’t use Result if no one cares about the error cases

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#5----dont-use-result-if-no-one-cares-about-the-error-cases

解釈

  • ドメインレベルで気にすることをエラーとして定義しろ
  • 決して「雨の日」を無視しろとは言ってない
  • 解釈した上での例:FileIOで殆どの場合、メモリ不足なんてドメインレベルで気にしない

んー、んー、んー

正しいそうな感じはするけど、ハンドリングしたいなー。。。それを抽象化するのはいい気がするんだよなー(Failedとして抽象化して上に渡せばよくない?そうするとスタックトレースがなー、スタックトレースを読み解きたくないなー)

  • 頑張った割に、見返りは少ないし、コードは複雑になる

それはそうかもしれない。。

#42 (comment)

try/catchのデメリットとして

可読性が落ちる(いちいちチェックしてるし)

と書いた

無いなら、無いで読みやすいではある

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

アプリのエラー(大体データ不整合、それが永続化前or永続化後でログレベルは変わるだろうけど)

システムのエラー

を分けようということだとは思う

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-6

Be careful when using Result for I/O errors

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#6----be-careful-when-using-result-for-io-errors

解釈

  • IOのエラーが全て例外か?というとそうではない
  • IOのエラーが全てアプリエラーか?というとそうではない
  • 結局自分のアプリドメインに依存するし、必要なものをエラーと定義しよう
  • ただ、I/Oはコアロジックから切り離そう

(この辺はビジネスロジックエラーを定義はするが、ロジックとしてはInfra層にもっていけば良さそうな感じはする)

そうなると、インフラ層が返すエラー型は純粋なビジネスロジックエラーがいいかなー。。?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-7

Don’t use Result if you care about performance

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#7----dont-use-result-if-you-care-about-performance

解釈

  • パフォーマンスは落ちるかもしれないが、盲目的にResultを外すせというわけではない

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Against ROP-8

Don’t use Result if you care about interop

引用元

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#8----dont-use-result-if-you-care-about-interop

解釈

  • 殆どの OO言語はResult的なやつは無い
  • 関数型イディオムアプローチを矯正するのはやめよう
  • NULLも使うこともあるだろう

嫌だ

NULLにNULL以上の別の意味をもたせてしまうから駄目であって、それを検知する術が人の技術に依存するなら、まずそれをやめたい

宣言的パラダイムを導入することで、考慮漏れを機械的に発見できる方が、中長期的に見て保守性が高いと信じている

あーでもNULLを宣言的にハンドリングを強制できるなら、ありなのかな?

NULLも論理型の一つ的な?それはNULLか?Noneにならない?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

あ、今気づいた。。。

Domain Modeling Made Functional

by Scott Wlaschin

著者と記事書いた人が同じじゃん

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

技術的例外とビジネス例外を同じ例外階層構造に入れてはいけない

技術的例外は貫通させて...

ビジネス例外は準正常系なので...

引用元

例外設計における大罪の29p
https://www.slideshare.net/t_wada/exception-design-by-contract

解釈

特になし

ビジネス例外を準正常系と言い換えるか

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

大罪1:無視
大罪2:隠蔽
大罪3:濫用
大罪4:過剰防御

参考

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

解釈

過剰防御への対策である「契約による設計」

アプローチとしては

ドメインルールが保証された、ドメインオブジェクトであることが契約の事前条件でなかろうか

つまり、引数の型チェック。その型は基本ドメイン固有型である。ことを保証するとなんとなりそう
つまり引数型チェックが仕組みとない言語はゲフンゲフン

Rubyだとどうすべきかな。。?全部事前契約は出来てる前提??いや例外として投げちゃだめな場合多そうだな?

チェックが必要か?謎い。。


Stringプリミティブな場合。。どうしようか

内容によるか

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

引数として渡した値・オブジェクトは全て事前条件を満たしたと見るべきか?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

あー、ドメイン固有型じゃなくても事前条件を満たしているかをチェックしてもいいのか

そしてそれを、早期リターンしてもよいと

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

Rubyだと毎回メソッドに begin/end で事前条件のチェックが必要か?

Rails(OSS)とかではそうなってるようには見えない。。つまり契約プログラミングはしていない?それともしている?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 8, 2022

いやもう、Rubyでは一旦考えを放棄

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 9, 2022

入門例外:Haskellの例外

Haskellはいくつかの点でユニークな取り組みを行っています。例えば、Haskellは純粋な世界では例外機構を廃止しMaybe/Eitherを導入し、不純な世界では例外機構を導入しています。例外処理をある区分によって使い分けている意味で、Haskellの例外処理は先進的です。

引用元

https://qiita.com/Kokudori/items/3a953c00012408f76ab9#haskell%E3%81%AE%E4%BE%8B%E5%A4%96

解釈

あれ?まさにこういうことをやれということでは。。?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 9, 2022

自分なりのまとめ(仮)

  • 例外は有り

自分が指す言葉

言葉 説明、代替表現
正常系 - 晴れの日
- 基本コース
準正常系 - 雨の日
- 代替コース
- ビジネス例外
- 予期する例外
- 回復可能なエラー
- データとして定義されるエラー
異常系 - 技術的例外
- 予期しない例外
- 回復不可能なエラー

※UCDD本:UseCaseDrivenDevelopment本:ユースケース駆動開発実践ガイド
※UCDD本の「晴れの日」「雨の日」の解釈が間違ってるかも

契約プログラミング的な文脈

正常系 準正常系 異常系
契約内/外 契約内 契約内 契約外

優先度

  1. 準正常系と異常系を似た仕組みで可能限り扱わない(可能ならツールに依存しても良い)
  2. 正常系と準正常系を似た仕組みで可能限り扱う(可能ならツールに依存しても良い)
ツール/仕組み 正常系 準正常系 異常系
Kotlin Arrow.kt Right or Valid Left or Invalid throw Exception
Rust Result型 ok err panic
Go

参考

kawasimaさん: 例外設計

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 9, 2022

テストの文脈でよく使われる、正常系と異常系について

難しくしている原因:自分が指している「準正常系」と「異常系」をまとめているところだと思った

NG

  • 正常系
  • 異常系
    • バリデーションエラー等
    • DBコネクションエラー
    • I/Oエラー

OK

  • 正常系
  • 準正常系
    • バリデーションエラー等
  • 異常系
    • DBコネクションエラー
    • I/Oエラー

要は「異常系」ってひとくくりにしてはいけない

経験的な話

見てきたテストで「異常系」って書いてあるやつほとんどが「準正常系」な気がする

最終的にお客さんから見ると「準正常系」と「異常系」は同じになりやすい。。ここが辛いところかもしれない。。

蛇足の蛇足

この辺考えてたら、JSTQBの世界に入った。。

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 23, 2022

Microsoft: エラー管理

ドメインに固有の型のエラー ケースと無効な状態を表現する

例題:銀行口座からの出金(WIthdrwal)

F#

type MoneyWithdrawalResult =
    | Success of amount:decimal // OK
    | InsufficientFunds of balance:decimal // 残高不足
    | CardExpired of DateTime // カードの有効期限切れ
    | UndisclosedFailure // 原因不明の失敗

Kotlinで表現すると?

パターン1

sealed interface MoneyWithdrawalResult {
    data class Success(val amount: Long) : MoneyWithdrawalResult
    data class InsufficientFunds(val balance: Long) : MoneyWithdrawalResult
    data class CardExpired(val date: Date) : MoneyWithdrawalResult
    object UndisclosedFailure : MoneyWithdrawalResult
}

パターン2

sealed interface MoneyWithdrawalResult {
    sealed interface Ok : MoneyWithdrawalResult {
        data class Withdrawn(val amount: Long) : MoneyWithdrawalResult
    }
    sealed interface Error : MoneyWithdrawalResult {
        data class InsufficientFunds(val balance: Long) : Error
        data class CardExpired(val date: Date) : Error
        object UndisclosedFailure : Error
    }
}

ん、、、普通だ

パターン1、2の書き方ができることにあとになって気づいたけど、これだとArrowって不要になる?

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 23, 2022

モナド エラー処理を使用して例外を置き換えない

.NET ランタイムでの関連性と妥当性、および言語間のエコシステム全体に関して、次のように例外の主要な長所や側面を考慮することが重要です。

  • 詳細な診断情報が含まれており、問題をデバッグするときに便利です。
  • ランタイムやその他の .NET 言語によってよく理解されています。
  • セマンティクスのサブセットをアドホック ベースで実装することによって故意に例外を "回避" するコードと比較すると、重要な定型句を減らすことができます。

3つ目重要とか書いてある

Kotlinとかでいうと 「try/catch という定型句を減らすことができる」という解釈をした

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

契約による設計として型を使う

契約その3:事前条件は、引数の型で表明する
...
契約その4:事後条件は、メソッドの返す型で表明する
...

取得した記事が「お気に入り済み」か「お気に入りではない」の区別をする時、

ログイン済みのユーザーが存在していることが前提となっている

この時、渡されたユーザーIDが存在しているor存在していないことを型で表現したいが、とても難しいと考える

仮に「存在しなかった時」それは

  1. ビジネス例外
  2. 技術的例外

どちらか?少なくとも「2. 技術的例外」にしたくない感じがある

では「1. ビジネス例外」か?と聞かれると、ぐぬぬ感ある

とりあえず今は「ビジネス例外」とする。。。

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

ビジネス例外 -> 準正常系
技術例外 -> 異常系

ってすると後者感ある

ぐぬぬ

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

https://twitter.com/j5ik2o/status/1468515811438788610

ErrorとDefect(欠陥)は異なる

Resultで返す == リカバリ

仕様として欠陥扱いせず、Errorとしてハンドリングしたい => 設計の段階で精査

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

panic!すべきかするまいか on The Rust Programming Language 日本語版

これはpanicで良い例

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1".parse().unwrap();

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

Error, Defect, Fault, Failureの定義と解釈

これでも

  • Error
  • Defect
  • Fault
  • Failure

準正常系(ビジネス例外)と異常系(技術的例外)と2分類は全然していない。。(全部3つに近い)

こちらも3つにするべきか

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

アイディア

  • 正常系
  • ユーザーから見ると全部失敗
    • 準正常系
    • 準異常系
    • 異常系

@sunakan
Copy link
Collaborator Author

sunakan commented Jul 27, 2022

ISTQBより

A human action that produces an incorrect result.
Synonyms: mistake

An imperfection or deficiency in a work product where it does not meet its requirements or specifications.
Synonyms: bug, fault

An event in which a component or system does not perform a required function within specified limits.
Synonyms:

実際にテストする時。。(理想形として)

Success:単体テストする、E2Eする
Error:単体テストする、頻度が高いE2Eとしてする?
Defect:単体テストする、E2Eしない
Failure:単体テストしない、E2Eしない(できない?)

@sunakan
Copy link
Collaborator Author

sunakan commented Sep 20, 2022

arrow.core.Either 使い方メモと、初めてのKotlinコードリーディング > 2. Either#fx

UZABASEさんのテックブログにて少し古いらしいが Arrow.kt のEitherの使い方で、左型をExceptionにしている。。。

そしてEither.fxっていうのを使ってtry/catchを完全に消している?
これが今も有効か、エラーハンドリングをいい感じにしていれくれるかは謎

ただ、bindで早期リターンまがいなやつができるのは魅力的

Scalaのforがそんな感じじゃなかったっけ

ムム、(技術的例外)異常系と(ビジネス的例外)準正常系の扱いについて、振り返り・再検討するべきか 🤔


UZABASEさんはNewsPicksの課金基盤でArrow.ktを利用しているらしい 👍

@sunakan
Copy link
Collaborator Author

sunakan commented Sep 20, 2022

  • 正常系
  • 準正常系
  • 異常系

4種類ほしくなってきた。。。

正常系:OK
準正常系:ドメインエラー
準異常系:ロジック上絶対ありえないやつ(Unexpectedとか、500)だけど、型の操作的に可能性として残ってしまうやつ(テスト不要)
異常系:技術的例外

準異常系をUnexpectedとして準正常系に含め、異常系と分け方を明文化できたら良いかもしれない自作のException作って投げるべきかも?

Kotlinのデストラクタがもっと強力になってほしすぎる

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant