Result型とRailway Oriented Programmingをめぐる旅

まとめ(長い)

  • JavaScript からの POST エラーで必要以上に広く例外を捕捉していたため、実際に起きたアプリケーションエラーには対処できず、かつ POST できていない事態も検知できていないという障害があった
  • (少なくともアプリケーションレベルのコードにおいては)例外は監視対象の障害レベルのエラーのみに利用し、アプリケーションエラーのレベルは例外を使わずにエラー状態を伝播させる方法はないか1
  • Result 型が一部では流行っているらしい
  • Railway Oriented Programming という考え方があるらしい
  • 最近また TypeScript 界隈は関数型が流行っているらしい(なんか定期的に流行るけど定着はしないよね)
  • Railway Oriented Programming って別に関数型の機能をフルに利用しなくてもよいのでは?
  • Ruby なら dry-operation でまずますの領域をカバーできそうなので、この辺から取り入れてみてもよさそう

この間、半年ほど。障害の対処は終わり、対策としてあれこれ画策しつつ、では根本的な実装方針はどうあるべきか?は趣味でじっくりめに取り組んでみた。

きっかけの話

障害がありまして。その対策から以下のような取り組みをしていた。

問題意識

上記の時点での問題意識は

アプリケーションレベルのエラーとI/Oなど下のレイヤーの障害レベルのエラーが同列に「例外」 になってしまうのはよくない

というもの。

アプリケーションレベルのエラーは、そのエラー状態を取り除いて目的を達成するためにエラーの内容をユーザーにフィードバックしてユーザーに問題を解消してもらう必要があったりする。つまり、

エラーの情報について、処理のレイヤーからフローを制御するレイヤーに戻し、ユーザーとのI/O(UIなど)に伝播させたい

という要望がある。例外は大域脱出に利用できるのでカジュアルにこの制御をジャンプする方法として利用できるが、素朴に例外を使って制御してしまうと、今度は

意図していない例外を捕捉してしまった場合に、本来はちゃんと監視対象としたい障害レベルのエラーが監視できなくなる

という問題を新たに生んでしまう。

もちろん「きちんと例外をハンドリングできる訓練された者だけが例外を使っているのであれば」これでも問題はないと思う。Custom Error を適切に定義し、意図した例外以外を捕捉しないように丁寧にコーディングできるのであれば。しかし、実際にはこれを徹底するのは難しい。特に例外のクラスそのものを常に利用できるわけではない JavaScript では文法レベルで必要以上に広範囲の例外を捕捉してしまいやすいという問題を抱えている2

そこで

例外を使わずにエラーを伝播させてフローを制御する方法がないものか

と考えるようになった。

この時点で例外中立という考え方にも触れ、まぁそうだよなぁと思うようにはなったものの、例外以外のエラー伝播方法がないことには始まらないよな、という感覚。

まずResult型を試してみた

TypeScriptでResult型でのエラーハンドリングを通してモナドの世界を覗いてみる #関数型プログラミング - Qiita

この辺りを参考にしながら、個人的なプロジェクトで実験し始めた。2024年の10月辺り。

  • まずライブラリは一切使わず、初めてまともにジェネリクス型を自分で定義してみるなど、素朴にプログラミングを勉強している状態
  • ジェネリスクをゼロから定義し始めるのはちょっとコストでかいし、人によって、プロジェクトによって好きなように Result 型が作られると明らかにまずそう
  • いい感じに書ける気がするけど、なんか、isOk() とか obj.ok とか書きまくりになって、むしろコード読みにくくなってない? どうだろう?

Result 型自体はなんとなくよさげと思える一方、エラー情報の伝播方法にルールがなく、結局一つ一つ丁寧に分岐を書きまくらなければいけない。これはあんまり嬉しくないなと感じていた。

Rubyでもやってみよう

あれこれ JavaScript / TypeScript で調べ物してみたけど、実際には急に JavaScript / TypeScript のコードが増える予定もなく、障害のあったプロジェクトでは現在は不必要に例外を使ったフロー制御のコードを削除し、監視もうまくできている。

一方で既存のコードもちょこちょこあって、今後もちょこちょこ増えていきそうな Ruby についてはどうしたもんかと考えた。

詳細は dry-operationのススメとエラー情報をViewまで持っていく方法の模索 (2025-01-26) | あーありがち に譲ることとして、結論としては Ruby も TypeScript 同様、標準の道具として Result 型は存在しないので、何かを入れるしかないんだけど、

  • dry-operation を入れる
  • dry-operation が依存している dry-monads を利用する
  • 成否は Sucess()Failure() を返せ、だけを守ればよい
  • ついでに Dry::Monads::Maybe がついてくるので Some()None() を使うことで nil を踏み抜く心配も減る
  • ハッピーパスを step 記法で書くと Result の判定は自分で書かなくて済む
  • operation 全体の成否はいい具合に Dry::Monads::Result で返ってくる

かなりありがたい感じにまとまっている。

関数型とモナドと静的型付けの話にたどり着く

上記の Ruby の話を踏まえてもう一度 TypeScript に戻ってくると、dry-operation のようにいい具合のまとめ方をしているツールって、どうも存在しないらしい。まぁ確かに JS/TS は Ruby と違って async/await など考えることが増えるので、これ一つあればシュッと解決しますよ、みたいなことを言うのは少し大変そうだ。

TypeScript開発にRailway Orientedを持ち込み、より型安全なエラーハンドリングへ - Sansan Tech Blog

例えば上記の取り組みは関数型の方向で頑張ろうしているように読める。

  1. Result 型を受け付けるための関数合成
  2. 関数型の pipe

を導入しようとしている。関数型の pipe は入出力を連結させてしまうので複数の値をもとにフローを制御しようと思った場合、その複数の値を持ち回す必要がある。要するに Context を導入して全部 Context にぶら下げましょう、みたいな感じになる。3

またこれをきっかけにモナドの理解も進み、

  • Result 型は Rust が起源ぽく、Kotlin や Swift にもある
  • モナド的には Either モナドになる(この場合は Ok や Err という意味を持たない、どう使ってもよいが、普通は “Right” を Ok 扱いにするようだ)
  • そういや昔 Java にデフォルト引数がなくて Optional 型を使ったけど、これってもしかして Maybe モナドでは?
    • なんかモナドのような何かを表現する名前って、いくつかあってメンドイなぁ
  • モナド則を守って関数型プログラミングが必要?

モナドはメタファーではない · eed3si9n

といった辺りに辿り着いたところで踏みとどまる。

いやーだからモナド避けてたのよ。モナドと関数型の話を全部持ち込むのは仕事だとちょっと現実的なコストとは言い難いなぁ。

もう一つ、関数型ってモナドモナド言うのが好きじゃないのもあったんだけど、全部関数だと粒度の違いを認識しにくくて苦手だなと感じていた。これについては「フローを担う型」「個別の処理を担う型」を分けてあげることで表現可能という話に生成 AI と議論しているなかで辿り着いた。まーなるほどそれは確かにそうなのかも。

  • パッケージ、モジュールなどのその言語固有の名前空間の機能
  • 粒度の大きな型、粒度の小さな型の命名と使い分け

これがあればクラスベースオブジェクト指向のように粒度の整理は確かに可能だ。なるほど、関数型と静的型付けがセットのように言われることに長いこと違和感があったんだけど、現実解の落としどころとしてはよい組み合わせかもしれない。

もう一度世の中の情報を漁り直す

とは言え、標準機能にない関数型プログラミングの方向に寄せるのはいろいろ大変そうなので少し距離を置きたいと感じていたところ、以下のような記事を見つけた。正確には、以前も読んでいたんだけど、その時は問題のフォーカスが自分の中でピタッと合っていなくてなんとなく流し読みしただけになっていたもの。

ちょうど1年くらい前の記事。ぼんやりと覚えている程度だったけど、読み返すとそうだなという感じ。また、自分はイベントなどは追っていなかったが、あとで調べ直すとちょうどそれっぽいタイミングで話題のテーマの一端を追っていたらしい。

また、もう少し遡ると Result が流行り始めた辺りで悩ましい気持ちを吐露する意見はいくつもあったようだ。

改めてRailway Oriented Programmingをちゃんと読んでみる

ここでようやく dry-operation の前身である dry-transaction で触れられている Railway Oriented Programming を読むと、

Railway Oriented Programming | F# for fun and profit

実はこのエントリでも

First, this post is not trying to be a monad tutorial, but is instead focused on solving the specific problem of error handling.

Most people coming to F# are not familiar with monads. I’d rather present an approach that is visual, non-intimidating, and generally more intuitive for many people.

I am strong believer in a “begin with the concrete, and move to the abstract” pedagogical approach. In my experience, once you are familiar with this particular approach, the higher level abstractions are easier to grasp later.

Second, I would be incorrect to claim that my two-track type with bind is a monad anyway – a monad is more complicated than that, and I just didn’t want to get into the monad laws here.

Third, and most importantly, Either is too general a concept. I wanted to present a recipe, not a tool.

  • この投稿はモナドのチュートリアルを目指しているわけではなく、その代わりにエラーハンドリングという特定の問題の解決に注目している
  • ほとんどの F# プログラマはモナドに明るくない
  • モナド則に立ち入りたくない
  • Either は汎用的すぎる概念で、Either という道具を紹介することではなく、(エラーハンドリングの)レシピを紹介したかった

と書かれている。

で、あれば先日紹介した dry-operation は Railway Oriented Programming の Ruby らしい解釈としてはアリじゃないだろうか? やりたいことは繊細じゃないエラーハンドリングなのだ。

じゃあ結論は

まず Ruby はもう dry-operation でいいと思う。そんなに複雑に考えすぎる必要はない。Result 型で表現することで都合のいいことが増えそうな手応えはある。

TypeScript については

辺りを入れてみようと思う。andThen なんかも Promise の延長のように扱えるので、悪くないかも。でも入出力が繋がっているので複数の値を扱おうと思うとどうすんのかは考えないといけない。関数自体は独立性高く作っておいて、そのフローに関係する値を処理するだけの関数を挟む、みたいな方法もあるかもしれない。

いやいや、静的型付けで型安全がいいんじゃないすか?

TypeScript の型システムは型安全性を保証しませんよと

『n月刊ラムダノート』Vol.4 No.3(2024)発行のお知らせ – 技術書出版と販売のラムダノート

に書かれています。

なんかいろいろコンプレックスが解消された気分。動的型付け言語も型安全だし、静的型付けでなければいけないわけでもないし関数型でなければいけないわけでもない。必ずしも関数型向きの題材を扱っているわけでもなく、単にエラーハンドリングから繊細さを取り除き、エラー情報を扱いやすくしたいだけなので、それに合った道具、考え方をすればよい。その結論が関数型に寄せるという方針のプロジェクト、チームがあってもそれは否定されるべきものではないが、絶対の正解というわけでもないと、今は考えている。

  1. ライブラリが例外を使っているのは仕方ない。これは可能性のある例外をちゃんと列挙して影響範囲を閉じ込めるなり、そのままスルーで例外中立を守るのがよさそう。 

  2. JavaScript の記法の問題にの詳細については 少しでも例外を安全に扱うために - RubyとJavaScript編 - (2024-08-10) | あーありがち の JavaScript の部分を参照してほしい。 

  3. これって実はそれぞれの関数の独立性が下がってしまうのではないかと個人的に感じている。関数型って言うほど関数の再利用性高い?って感じるポイントの一つでもある。 

More