鉄道指向プログラミングとは|Result型で書くエラーハンドリング入門
鉄道指向プログラミング(Railway Oriented Programming、ROP)は、処理を「成功」と「失敗」の2本のレールに分け、失敗したら以降の処理を自動でスキップさせるエラーハンドリングの設計手法です。F#コミュニティのScott Wlaschinが2010年代前半に提唱・普及させ、いまではRustやF#など関数型プログラミングの基本概念を取り入れた言語で定番の考え方になっています。中心になるのが、成功値かエラー値のどちらかを持つResult型です。この記事では、レールをつなぐmap・bind・mapErrの役割から、F#・Rust・TypeScript・C#・Pythonの実装、try-catchとの使い分けまでを具体コードで整理します。
まとめ:鉄道指向プログラミングの要点
- 2本のレール:処理が成功すれば成功レール、失敗すれば失敗レールに移り、一度失敗レールに入ると後続処理はスキップされる(ショートサーキット)。
- Result型が線路:
Ok(成功値)かErr(エラー値)のどちらかを表す型。エラーが戻り値の型に現れるため、握りつぶしを型で防げる。 - つなぎ役は3つ:
map=成功値の変換、bind=失敗しうる処理の連結、mapErr=エラー値の変換。 - 言語対応:RustとF#は標準機能、TypeScript・C#・PythonはライブラリでResult型を導入する。
- 万能ではない:例外前提の標準ライブラリと二重管理になる場面や、複数エラーの集約が必要な場面には向かない。
鉄道指向プログラミングとは|2本のレールでエラーを扱う考え方
鉄道指向プログラミングの名前は、鉄道の分岐器(ポイント)に由来します。列車が本線か側線かに振り分けられるように、各処理は入力が1本で、出力が「成功」「失敗」の2本に分かれる関数(スイッチ関数)として設計します。成功なら本線(成功トラック)を進み、失敗なら側線(失敗トラック)へ移り、そこからは合流せず終点まで失敗のまま流れます。
成功トラックと失敗トラックの分岐
従来の手続き的なコードは、各ステップの直後にifでエラーを確認し、ネストが深くなりがちです。鉄道指向では、失敗トラックに入った時点で後続のステップが自動でスキップされるため、正常系のロジックだけを一直線に記述できます。この「失敗が最後まで伝播してスキップされる」挙動をショートサーキットと呼びます。
提唱者と背景:F#コミュニティのScott Wlaschin
この比喩は、Scott Wlaschinが解説サイト「F# for Fun and Profit」と一連の講演で広めたものです。関数型言語では、失敗しうる処理の戻り値をResult型やOption型で表し、例外を投げずに値として扱う文化があります。鉄道指向プログラミングは、その値ベースのエラーハンドリングを「レールの合成」という直感的な図式に落とし込んだ点で普及しました。
Result型が「レール」になる仕組み|map・bind・mapErrの役割
Result型は、処理結果を成功(Ok)か失敗(Err)のどちらか一方で表すデータ構造です。プログラミングで「result」がこの文脈で使われるときは、単なる戻り値ではなく「成功値かエラー値を包んだ箱」を指します。この箱をつなぐ関数が3つあり、役割を分けて覚えると混乱しません。
| 関数 | 渡す関数の型 | 用途 |
|---|---|---|
map |
成功値 → 普通の値 | 成功レールの値を変換する(失敗しない処理) |
bind |
成功値 → Result | 失敗しうる処理を連結する(別名 flatMap / andThen) |
mapErr |
失敗値 → 別の失敗値 | エラー情報を変換・付加する |
bindによるスイッチ関数の連結
「バリデーション」「DB保存」のように失敗しうる処理は、戻り値がResultになります。この2本出力の関数をそのまま連結するための接着剤がbindです。前の処理が成功なら中の値を次の関数に渡し、失敗ならその関数を呼ばずにエラーをそのまま次へ流します。一方、mapは「消費税を掛ける」のような必ず成功する変換に使い、mapErrは失敗レールの値だけを整形します。この使い分けが鉄道指向プログラミングの核心です。
主要言語でのResult型と鉄道指向プログラミングの実装
Result型は言語によって「標準機能」か「ライブラリ」かが分かれます。RustとF#は言語・標準ライブラリに組み込まれ、TypeScript・C#・Pythonはライブラリを導入して使うのが実務的です。
F#:標準のResult型とResultモジュール
F#は4.1(2016年)でResult型を標準化しました。ケースはOkとErrorで、Result.bind/Result.map/Result.mapErrorが用意されています。パイプ演算子と組み合わせると、レールの合成がそのままコードの見た目になります。
// F# 標準のResult型(F# 4.1以降)
let validateAge age =
if age >= 0 then Ok age else Error "年齢が不正です"
validateAge 20
|> Result.bind checkAdult // 失敗しうる処理を連結
|> Result.map (fun a -> a + 1) // 成功値だけ変換
|> Result.mapError (fun e -> $"入力エラー: {e}")
Rust:Result型と?演算子(言語機能として組み込み)
RustはResult<T, E>を標準型として持ち、Ok(T)とErr(E)で表します。特徴は?演算子で、失敗したその場でErrを早期returnして呼び出し元へ伝播します。これはbindによる失敗トラックへの分岐を言語構文で表現したもので、鉄道指向プログラミングが言語機能として組み込まれている例です。
// Rust: Result<T, E> と ? 演算子
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse::<i32>()?; // 失敗したら即座にErrをreturn
Ok(n * 2)
}
TypeScript:neverthrowでResult型を導入
TypeScriptに標準のResult型はないため、neverthrowライブラリがよく使われます。ok/errで値を包み、map・andThen(bind相当)・mapErrで連結します。非同期にはResultAsyncがあります。TypeScriptの型と型推論のユニオン型で、成功と失敗を1つの型として表現できます。
import { ok, err, Result } from "neverthrow";
const validate = (n: number): Result<number, string> =>
n > 0 ? ok(n) : err("正の数ではありません");
validate(5)
.andThen(n => ok(n * 2)) // 失敗しうる処理を連結(bind相当)
.map(n => n + 1) // 成功値だけ変換
.mapErr(e => `検証エラー: ${e}`);
C#:CSharpFunctionalExtensionsのResult
C#にも標準のResult型はなく、実務ではCSharpFunctionalExtensionsが定番です。Result.Success/Result.Failureで生成し、Bind・Map・MapErrorで連結します。C# 9以降のrecordで自作する選択肢もありますが、合成用のメソッドまで含めるとライブラリが手早いです。
using CSharpFunctionalExtensions;
Result<int> Validate(int n) =>
n > 0 ? Result.Success(n) : Result.Failure<int>("正の数ではありません");
Validate(5)
.Bind(n => Result.Success(n * 2)) // 失敗しうる処理を連結
.Map(n => n + 1) // 成功値だけ変換
.MapError(e => $"検証エラー: {e}");
Python:dry-python/returnsのResult
Pythonではreturns(dry-python)がResult・Success・Failureを提供します。bindで連結し、flowで処理を左から右へ並べられます。例外を投げる既存関数は@safeデコレータでResultに包めるため、例外文化のPythonにも段階的に導入できます。
from returns.result import Result, Success, Failure
from returns.pipeline import flow
from returns.pointfree import bind
def validate(n: int) -> Result[int, str]:
return Success(n) if n > 0 else Failure("正の数ではありません")
flow(
validate(5),
bind(lambda n: Success(n * 2)), # 失敗しうる処理を連結
)
従来の例外処理(try-catch)との違い|使い分けの基準
鉄道指向プログラミングと従来の例外処理(try-catch)の最大の違いは、エラーが「型に現れるか」です。例外は関数のシグネチャに現れず、catchを書き忘れても呼び出し側は気づけません。Result型はエラーが戻り値の型に含まれるため、処理し忘れをコンパイラや型チェックで検出できます。
| 観点 | 例外(try-catch) | Result型(鉄道指向) |
|---|---|---|
| エラーの所在 | 型に現れない | 戻り値の型に現れる |
| 制御フロー | throwで大域脱出 | 通常の値として連結 |
| 処理忘れ | catch漏れを防げない | 型で処理を強制できる |
| 主な言語 | Java・C#・Python | Rust・F#(Goは値でエラー) |
もっとも、例外にも利点があります。予期しない致命的エラー(メモリ不足など)は、その場で処理を止めて上位に伝える例外の方が素直です。実務では「業務上あり得る失敗はResultで型に載せ、想定外の異常は例外に任せる」と役割を分けます。Go言語のエラーハンドリングのように、戻り値でエラーを明示的に返す言語は、この考え方に近い設計です。
鉄道指向プログラミングを採用すべきでない場面
鉄道指向プログラミングは万能ではありません。次の場面では、無理に持ち込むとコードがかえって複雑になります。
- 言語とライブラリが例外前提のとき:Pythonや標準的なJavaのように、周辺ライブラリがすべて例外を投げる環境でResult型を混在させると、例外とResultの二重管理になります。境界(外部API呼び出しやフレームワークの入口)だけでResultに変換するのが現実的です。
- 失敗が1種類で1関数に閉じるとき:ファイルを1つ開くだけといった単純な処理は、try-catchや
?一発で十分です。合成のためのResultは過剰です。 - 複数のエラーをまとめて返したいとき:入力フォームの全項目のエラーを一度に集めたい場合、鉄道指向は最初の失敗で止まる1本道のため不向きです。全エラーを蓄積するにはApplicativeスタイルのValidation型が必要になります。
言い換えると、鉄道指向プログラミングが最も効くのは「失敗しうる処理を何段も直列につなぐパイプライン」です。バリデーション→変換→保存のような一連の流れを、正常系だけ読めるコードにしたいときに導入価値があります。
よくある質問
Result型とOption型(Maybe型)の違いは?
Option型は「値があるか無いか」だけを表し、失敗の理由を持ちません。Result型は失敗時にエラー値(メッセージや型)を保持できます。理由を伝えたいエラー処理にはResult、単に有無だけならOptionを使い分けます。
プログラミングで「result」はどういう意味ですか?
鉄道指向の文脈では、単なる戻り値ではなく「成功値かエラー値のどちらか一方を包んだ型」を指します。Ok/Err(言語によりOk/Error、Success/Failure)の2状態を持つ点が普通の変数と異なります。
C#に標準のResult型はありますか?
標準ライブラリにはありません。CSharpFunctionalExtensionsやLanguageExtなどのライブラリを導入するか、C# 9以降のrecordで自作します。合成用のBind/Mapまで欲しい場合はライブラリが手早いです。
Pythonで鉄道指向プログラミングを実装するには?
returns(dry-python)ライブラリのResult・Success・Failureを使い、bindとflowで連結します。既存の例外を投げる関数は@safeデコレータでResultに包めます。
鉄道指向プログラミングと関数型プログラミングの関係は?
鉄道指向は関数型の「モナド(bindで合成できる型)」の考え方をエラーハンドリングに応用したものです。モナドの理論を知らなくても、map・bind・mapErrの3つの使い分けさえ押さえれば実装できます。