代数的データ型 & kotlin-result
TypeScript のような静的型付けを持つ言語では、開発者が定義するカスタム型の多くが代数的データ型(ADT)の概念を利用しています。TypeScript は和型(Sum Types)や積型(Product Types)の形式で代数的データ型をサポートしており、これにより開発者はより柔軟かつ安全にプログラムを設計することができます。
和型(Union Types)
TypeScript では、和型は複数の型のいずれかを取ることができる型を定義するのに使われます。これは | 演算子を使用して表されます。
積型(Intersection Types)
積型は複数の型を組み合わせて新しい型を作ることができます。これは & 演算子を使用して表されます。
代数的データ型って怖くなくて、単にいつも TypeScript で定義している型をイメージしたらいい。
Union と Intersection を使った型定義。
別の話になるが、タグ付きユニオン
判別にテンプレートリテラルが使えるようになっていたり (ts 4.5)
type OkResult<T> = {
type: `ok_${string}`;
payload: T;
};
type ErrorResult = {
type: `error_${string}`;
payload: Error;
};
判別した後に、他の型が絞りこまれるようになっていたり (ts 4.6)
function unwrapResult<T>(result: Result<T>): T {
// payload はここでは T | Error 型
const { type, payload } = result;
if (type === "ok") {
// payload はこの中では T 型
return payload;
} else {
throw payload;
}
}
kotlin-result
そもそもなぜ Kotlin にこのような代数的に処理できる機構が必要なのか
なぜResult型を使うのか
・関数シグネチャで、呼び出し元が処理すべきエラーを伝えることができる
・呼び出し元にエラー処理を強制できる
・復帰可能なエラーと復帰不可能なエラーを明確に区別できる
・エラー処理を簡潔に書ける
なぜ Kotlin では検査例外の仕組みを持っていないのか
Kotlinが検査例外(checked exceptions)の仕組みを持っていない理由は、Kotlinの設計哲学と開発者体験に基づいています。検査例外はJavaで広く知られている機能で、メソッドが投げる可能性のある例外を明示的に宣言し、それを呼び出す側でキャッチするか、さらに上位にスローすることを強制します。しかし、Kotlinの開発チームは以下の理由から検査例外を採用していません。
Kotlinの設計は、開発者がより簡潔で読みやすく、メンテナンスしやすいコードを書くことを促進することに重点を置いています。検査例外を含めないことで、この目標に寄与しています。
開発者体験の向上: 検査例外はコードの可読性を低下させ、書くべきコードの量を増やします。Kotlinでは、よりシンプルで読みやすいコードを書くことができるようにすることを目指しています。
柔軟性の確保: 検査例外は、例外をキャッチして処理するか、宣言してさらに上のレイヤーに伝播させるかを開発者に強制します。これにより、ライブラリの設計者がAPIの使用方法を事前に完全に理解していなければならず、将来的にAPIを変更する際の柔軟性が低下します。
実用性の向上: KotlinはJavaのコードと相互運用するように設計されています。Javaの検査例外をそのままKotlinに適用した場合、相互運用時に不都合が生じる可能性があります。Kotlinでは例外を検査しないことで、Javaコードとのスムーズな統合を実現しています。
実践的なエラー処理: 検査例外が意図するところは、エラー処理を強制することによってより堅牢なアプリケーションを作成することですが、実際には開発者が例外処理を適切に行うことを保証するものではありません。Kotlinでは、開発者がより実践的かつ柔軟にエラー処理を行えるようにすることを目指しています。
get() / getError()
戻り値: T? / E?
不確定
unwrap() / unwrapError()
戻り値: T / E
確定
onSuccess() / onFailure()
引数: (T) -> Unit / (E) -> Unit
戻り値: Result<T, E> / Result<T, E>
返す型は変換しない
map() / mapError()
引数: (T) -> U / (E) -> F
戻り値: Result<U, E> / Result<T, F>
返す型を変換する
andThen() / orElse()
引数: (T) -> Result<U, E> / (E) -> Result<T, F>
戻り値: Result<U, E> / Result<T, F>
ラムダ式の返り値が Result
返す型を変換する
処理を行い、その処理の結果を返すことを意図している
flatMap()
引数: (T) -> Result<U, E>
戻り値: Result<U, E>
関数としては andThen() と同じ
セマンティックの違い
flatMap() は中で andThen() を呼んでいるだけ。
コメントにも書かれている。
Elm の Result を kotlin-result で書くとどうなるか