Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Result型使っていますか? 使っている 知っているけど使っていない 初めて聞いた 2/60

Slide 3

Slide 3 text

Result型の流行? 色んなカンファレンスや技術記事などで Result 型を推す声 一方で懐疑的な声も... 「標準で実装されていないから使うべきでない?」 「学習コストが高そう」 「既存コードとの共存は?」 3/60

Slide 4

Slide 4 text

本セッションで話すこと 実際どうなの?どう設計するのが良いのか? 自分のプロジェクトで採用すべきか判断できるようになる 4/60

Slide 5

Slide 5 text

梶川 琢馬 𝕏 @kajitack 所属: 株式会社 TechBowl 仕事: VPoT / TechTrain 開発 / メンター 前職: ゲーム / SaaS / EC / SNS 開発など 言語: PHP / TypeScript / Go 富山県高岡市出身 / 東京在住 BuriKaigiの参加は2回目! 前回は「例外処理を理解する」というテーマで登壇 5/60

Slide 6

Slide 6 text

6/60

Slide 7

Slide 7 text

前提と対象 前提 例外処理: try-catch 構文。および類似の構文 Result 型: 戻り値によって成功か失敗を表す。Either 型や Try 型とも呼ばれる 対象 例外処理を持つ言語でのエラー設計について扱います。 なので、そもそも例外処理を持たない言語は対象外です。 7/60

Slide 8

Slide 8 text

Result型の基礎 8/60

Slide 9

Slide 9 text

Result型の構造 構造はシンプル 成功すれば T 型の値、エラーの場合は E 型の値を持つ型 成功 ( Ok ): 正常な値を返す 失敗 ( Err ): エラー情報を返す enum Result { Ok(T), Err(E), } 成功した場合、失敗した場合など処理を分岐できる 9/60

Slide 10

Slide 10 text

生成 成功 ( Ok ): 正常な値を返す 失敗 ( Err ): エラー情報を返す // 成功なら Ok(値) を返す if let Some(value) = input { return Ok(value); } // 失敗なら Err(エラー) を返す Err("Input cannot be null") 10/60

Slide 11

Slide 11 text

値として取り出す チェック用メソッド: is_ok() : 成功かどうかを判定 is_err() : 失敗かどうかを判定 値の取り出し: unwrap() : 成功時の値を 取り出す(失敗時はパニック) unwrap_err() : 失敗時のエラーを取り出す if result.is_ok() { let value = result.unwrap(); } else { let error = result.unwrap_err(); } 11/60

Slide 12

Slide 12 text

値として渡す Result 型を返す関数を合成する validate_user(user_id) .and_then(|user| check_stock(product_id, quantity)) .and_then(|_| process_payment(user_id, price)) .and_then(|_| create_order(user_id, product_id, quantity)) .map(|order| order.id) // 注文IDに変換 .map_err(|e| format!("Order failed: {}", e)) .unwrap_or(OrderId::default()) 主要なメソッド: and_then : Result 型を返す関数を チェーン(失敗時は即座に伝播) map : 成功時の値を変換 (Result → Result) map_err : エラーの値を変換 (Result → Result) unwrap_or : 失敗時にデフォルト値を返す 12/60

Slide 13

Slide 13 text

Railway Oriented Programming (ROP) Result 型を使った関数合成パターン 各ステップが失敗すると、そこで処理が中断され、エラーが返される 成功時のみ、次のステップへ進む(線路のように一方向に進む) Railway Oriented Programming: a functional approach to error handling https://speakerdeck.com/swlaschin/railway-oriented-programming-a-functional-approach-to-error-handling 13/60

Slide 14

Slide 14 text

try-catchとの比較 try-catch function processUser(userId: string) { try { const user = findUser(userId); const email = user.email; sendMail(email); return "Success"; } catch (error) { console.error(error); return "Failed"; } } 各関数が例外を投げる可能性があるが、 型からは分からない Result 型 fn process_user(user_id: &str) -> Result { find_user(user_id) .and_then(|user| Ok(user.email)) .and_then(|email| send_mail(email)) .map(|_| "Success".to_string()) } 失敗の可能性が型で明示的 エラーハンドリング漏れはコンパイルエラー 14/60

Slide 15

Slide 15 text

なぜResult型が生まれたのかを知ろう 技術の背景や流れを知ることで、どういう課題を解決するものなのかを知る 何が変わり、何が変わらないかを押さえることで設計判断に役立てる 15/60

Slide 16

Slide 16 text

エラーハンドリングの歴史 フェーズ アプローチ 特徴 黎明期 正常値=0、それ以外の値はエラー エラー = 停止、またはステータスコード 戻り値のチェックが任意 統一されてないので各メソッドで返す値がバラバラ エラー処理の体系化 例外(Exception) 大域脱出による分離、終了モデルの確立 Lisp, C++, Java, PHP, C# ... エラーは値 Result型、多値返却 成功/失敗を型で表現(Rust, Go, Haskell) コンパイラやLintで処理を強制、制御フローが明示的 例外処理が導入されることによってエラー処理が体系化された 参考 エラーハンドリングの歴史 https://faithandbrave.github.io/article/error_handling.html 例外を初めて実装した言語 https://yosuke-furukawa.hatenablog.com/entry/2021/12/24/145027 16/60

Slide 17

Slide 17 text

例外を使う際に出てきた課題 例外を投げることが副作用であり、明示的でない try-catch 書かなくても呼び出せてしまう (一応、検査例外や Doc で記述するパターンも有る) 大域脱出でGOTO文のようにフローが複雑になりがち どこで例外が投げられるか追いにくい 17/60

Slide 18

Slide 18 text

例外処理は「例外」のときのみ使う 制御フローに使うべきではない まとめてチェック try { const foo = runTask1(); const bar = runTask2(); } catch(e) { console.log('Error:', e); } 全部チェック try { const foo = runTask1(); } catch(e) { console.log('Error:', e); } try { const bar = runTask2(); } catch(e) { console.log('Error:', e); } https://typescript-jp.gitbook.io/deep-dive/type-system/exceptions 18/60

Slide 19

Slide 19 text

値で返す方式に原点回帰? 原点回帰ではなく進化 エラーを値として使いやすくなる 副作用ではなく、エラーも通常の値として扱える 関数合成で処理を連鎖できる 成功/失敗の分岐が型で明示的になる 関数のシグネチャから失敗の可能性が分かる コンパイラ/静的解析でチェックできる 19/60

Slide 20

Slide 20 text

関数型プログラミングの スタイルにマッチ ビジネスロジックが複雑になってきた現代に Railway Oriented Programming (ROP) が注目されている 関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう Scott Wlaschin (著), 猪股 健太郎 (著) 20/60

Slide 21

Slide 21 text

Result型を導入しよう 21/60

Slide 22

Slide 22 text

と思った矢先ですが... 例外処理からは逃げられない Result 型を取り入れても例外処理は使う(完全な置き換えではない) 回復不能なエラー、プログラム終了レベルも panic のような機構はないので、例外処理と して扱う 使わないように実装しても、ライブラリは例外を投げてくる 22/60

Slide 23

Slide 23 text

例外処理に頼りたい部分もある エコシステムでのエラーハンドリングの機構も使いたい トランザクションのロールバック HTTP ステータスコードの変換 ロギング・通知 23/60

Slide 24

Slide 24 text

うまく使い分けるには... Result型をドメインロジックに使う ドメインロジック: ドメイン知識を表現するルール 技術的な実装ではなく、業務上の制約や判断 ドメイン知識の中で想定できる失敗 24/60

Slide 25

Slide 25 text

ドメイン知識の中で 想定できる失敗 「例外」という名前が示す通り、本来は「例外的な」 状況に使うべき 失敗は想定できる結果であるため、その失敗を例外として モデリングすることは概念的におかしい 処理の失敗は「例外」ではなく、成功と同様に 考慮すべき「結果」 セキュア・バイ・デザイン 安全なソフトウェア設計 Dan Bergh Johnsson (著), Daniel Deogun (著), Daniel Sawano (著), 須田智之 (翻訳) 25/60

Slide 26

Slide 26 text

ドメインエラーと 技術的エラーの分類 ドメインエラー: ドメインロジックの一部 → 例外ではなく値として扱うべき 技術的エラー: システムの異常状態 → 例外で伝播 26/60

Slide 27

Slide 27 text

実践編 Result 型を使ったエラー設計 27/60

Slide 28

Slide 28 text

実践の流れ 1. エラーのモデリング - ドメインエラーを特定する 2. レイヤー分離 - 各層での責務を整理する 3. 実装 - リクエスト → ドメイン → レスポンス 28/60

Slide 29

Slide 29 text

例: 注文処理フロー 1. 入力値として商品 ID と数量を受け取る 2. 在庫確認と注文作成 3. 注文確定 29/60

Slide 30

Slide 30 text

処理の流れ 1. 入力値として商品 ID と数量を受け取る 2. 商品 ID と数量 ValueObject を作成 3. 注文 Entity を作成 4. DB に保存 30/60

Slide 31

Slide 31 text

エラーもドメインの 一部としてモデル化する ドメインをモデル化する際には、文字列などのプリミティブ型を 使わず、ドメインに特化した型を作成します。 さて、エラーも同じように扱われるべきです。 ドメインに 関する議論で特定の種類のエラーが挙がった場合、ドメイン内の 他の要素と同様にモデル化するべきです。 特別な対応が必要なエラーの種類ごとに個別のケースを用意する 関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう Scott Wlaschin (著), 猪股 健太郎 (著) 31/60

Slide 32

Slide 32 text

ドメイン エキスパートとの 会話で洗い出す 例: 注文作成の条件 商品の在庫が不足していたときの 対応を想定できる。 → ドメインロジック上で 起きるエラー 32/60

Slide 33

Slide 33 text

開発者のみが 関心を 持っている場合 例: データベースの接続中断 開発者の技術的な関心に のみ現れる → 技術的例外 33/60

Slide 34

Slide 34 text

モデル毎に制約を 図にまとめる 集約ごとに制約を記述して、どのような 違反があるかをドメインエキスパートと 認識を合わせる TechBowl流ドメインモデルでの情報設計 https://speakerdeck.com/hiroki_nakamura/techbowlliu-domeinmoderingu-2025-02-28 ドメインモデルの管理にYAMLを使ってみた https://zenn.dev/techtrain_blog/articles/cf6e9ce3634d51 34/60

Slide 35

Slide 35 text

エラーを扱う場所を整理する 35/60

Slide 36

Slide 36 text

エラーにも関心の分離と依存のルールを適用する 36/60

Slide 37

Slide 37 text

各レイヤーで投げる例外 ドメイン層で投げていた例外は Result 型で表現できそう 37/60

Slide 38

Slide 38 text

ドメインロジック ビジネスプロセスの結果表現 -> Result 型 アプリケーション ドメイン層の Result 型を受け取り、 Presentation に適した例外を投げる ここで投げた例外はフレームワークの ハンドラまでキャッチしない 38/60

Slide 39

Slide 39 text

処理の流れの中で例外とResult型の使い分け Entity の作成などのドメインロジックでは Result 型で表現する 一方、永続化の失敗は技術的エラーとして例外を投げることで即座にロールバックできる 39/60

Slide 40

Slide 40 text

リクエストを受けてドメイン層まで 40/60

Slide 41

Slide 41 text

ドメインモデルを常に正常に保つ 不正な状態を表現できないようにする / "Making illegal states unrepresentable" 無効な状態を型で表現できないようにし、失敗そのものが 起きない設計にする 想定外失敗は catch しない / "Let it crash" 想定内失敗と想定外失敗を分離、想定外は握りつぶさず 上位へ伝播させる 失敗を露出させることで原因特定が早く、監視と自動復旧で 運用として守る 境界で変換し、早く落とす / "Parse don't validate" / "Fail Fast" 境界で安全な型に変換して以後の分岐を消す 予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント https://speakerdeck.com/twada/growing-reliable-code-php-conference-fukuoka-2025 41/60

Slide 42

Slide 42 text

妥当性確認 妥当性確認には 2 種類ある 構文的(syntactical)な妥当性確認 ドメインモデルの状態に依存しない形式チェック → ValueObject やリクエストバリデーションで実施 意味的(semantical)な妥当性確認 ドメインモデルの現在の状態を踏まえたビジネスルール → ドメイン層で実施 手を動かしてわかるクリーンアーキテクチャ ヘキサゴナルアーキテクチャによるクリーンなアプリケーション開発 42/60

Slide 43

Slide 43 text

構文的な妥当性確認の実装 数量 ValueObject を作成 コンストラクタが呼ばれた段階で処理を中断できる ドメインモデルの状態に依存しない構文的なチェック 呼び出し側の実装ミスなので、例外を投げる class Quantity { public function __construct(int $value) { if ($value <= 0) { throw new InvalidArgumentException('数量は1以上を指定してください quantity: '.$value); } $this->value = $value; } } 43/60

Slide 44

Slide 44 text

リクエストのバリデーションを実装 例外処理は最初のエラーで中断してしまうため、すべてのエラーを一度に 検証できる validator を使用する。 // Laravelのバリデーションを使用した例 class CreateOrderRequest extends FormRequest { public function rules(): array { return [ 'productId' => ['required', 'integer'], 'quantity' => ['required', 'integer', 'min:1'] ]; } } 44/60

Slide 45

Slide 45 text

ドメイン層でResult型を取り入れる 45/60

Slide 46

Slide 46 text

Entityの作成をResult型で表現 失敗の種類を Enum で定義 enum OrderError { case ValidationError; case InsufficientStock; case PaymentFailed; case ShippingNotAvailable; } ワークフローで Result 型を活用 (Result 型は自作) /** * @return Result */ public function create(OrderRequest $request): Result { } 46/60

Slide 47

Slide 47 text

Entity作成に必要な処理の定義 Result 型を返す小さな関数に分割 function checkInventory(Order $order): Result { if (!hasStock($order)) { return Result::err(OrderError::InsufficientStock); } return Result::ok($order); } // processPayment, scheduleShipping なども同様に定義 47/60

Slide 48

Slide 48 text

小さな関数を組み合わせてビジネスロジックを作成 andThen で成功した時だけ次の処理を実行する class OrderFactory { /** * @return Result */ public function create( ProductId $productId, Quantity $quantity, CustomerId $customerId ): Result { return $this->validateOrder($productId, $quantity, $customerId) ->andThen(fn($data) => $this->checkInventory($data)) ->andThen(fn($data) => $this->processPayment($data)) ->andThen(fn($data) => $this->createOrder($data)); } } 48/60

Slide 49

Slide 49 text

レスポンスを返す 49/60

Slide 50

Slide 50 text

ユースケース側でResult型から例外への変換 Result 型のエラーを例外に変換し、フレームワークに委ねる $result = $this->orderFactory->create(...); if ($result->isErr()) { throw match ($result->unwrapErr()) { OrderError::InsufficientStock => new ConflictException(), OrderError::PaymentFailed => new PaymentRequiredException(), // ... }; } $order = $result->unwrap(); DB::transaction(fn() => $this->orderRepository->save($order)); return OrderDto::from($order); 50/60

Slide 51

Slide 51 text

HTTPレスポンスの変換 ドメイン層内(Semantic Validation - 意味検証): ユーザー操作で回復可能 → 400 系 例: 在庫切れ、重複登録 ドメインルール上定義されていない、捕捉できていないエラー → 500 系 ドメイン層の外での違反: Syntax Validation (形式検証) → 400 系 例: 必須パラメータ不足、型不一致 実装ミスや外部要因 → 500 系 例: DB 接続失敗、NullPointerException 51/60

Slide 52

Slide 52 text

実践した処理の流れをまとめると 入力層: 形式検証(構文チェック)→ validator ValueObject : コンストラクタで例外処理 不正な値を握りつぶさず、バグや境界の漏れを検出するため ドメイン層: 意味的検証(ドメインルール)→ Result 型 インフラ層: 例外処理(DB 接続失敗、ネットワークタイムアウト) アプリケーション層: Result のエラーを例外処理として投げ直し、フレームワークに委ねる 52/60

Slide 53

Slide 53 text

技術選定 53/60

Slide 54

Slide 54 text

もちろん、いいことばかりじゃない... Result型導入の注意点 単純なロジックだと、エラーを値として扱うことによる恩恵が受けられない ドメイン層が分離できていないと例外と混在してしまう 標準的でない記述による学習コスト Coding Agent へのコンテキスト共有が必要 関数型のパラダイムを無理に取り入れる必要はないが、取り入れない場合は冗長になる 54/60

Slide 55

Slide 55 text

どのライブラリを使うべきか? 基本的には自前で実装するのがオススメ ドメインロジックでライブラリ依存を避けたいので... そのうえで、選定するのであれば エラーの理由(種類)を定義できるか? 関数合成のための機能があるか? Result 型に特化しているシンプルな設計になっているか? 言語機能の進化で大きな影響を受けないか? 55/60

Slide 56

Slide 56 text

\ PHP での Result 型実装についてはこちらを参考にしてみてください / https://speakerdeck.com/kajitack/result-type-in-php 56/60

Slide 57

Slide 57 text

使っている言語の特性を知ろう 値を表現出来るか どちらか一方の状態しか取り得ない(直和型)を実現するための機構 Tagged Union: Ok か Err のどちらか一方のみを持つ型 継承 + ジェネリクス 値の取り出し 安全に取り出すための型ガードや静的解析によるチェックやパターンマッチに よって網羅できるか? 確認ポイント 代数的データ型をサポートしている?(直和型・直積型を組み合わせた型システム) 関数型スタイルを取り入れようとしている? もしくは将来的に実現する可能性があるのか? RFC や言語思想を確認してみるといいかも 57/60

Slide 58

Slide 58 text

いろんな言語のエラー処理に ついて調べてみよう Go の Errors are values という考え方 Java の検査例外、非検査例外 Swift のエラー4 分類 無効値の表し方 Nullable、Maybe、Optional... 設計のヒントになるかも? 58/60

Slide 59

Slide 59 text

本日のまとめです 例外は副作用で、制御フローに使うべきではない Result 型はエラーを値で表現することで明示的に扱える 複雑なドメインロジックでの制約は 例外としてではなく、明示的に扱いたい まず、エラーを明示的に扱い、次に 関数型のスタイルを取り入れる ドメインとI/Oを分離する設計が大事 ご清聴ありがとうございました! ぜひ、色んな言語の話を聞きたいです! 59/60

Slide 60

Slide 60 text

60/60