【TsKaigi】「TypeScript 関数型バックエンド開発のリアル」を聞きました

TypeScript 関数型バックエンド開発のリアル

tskaigi.org

伊藤 直也 様(@naoya_ito

株式会社 一休 / CTO

発表スライド

内容メモ

背景

バックエンドを関数型プログラミング寄りの実装で、飲食店システムのSaaSのバックエンドを開発をしているが、詳細についてイメージがわかないという話をよく聞くのでそれに答えるような話。

Domain Modeling Made Functional

概要は、クラスにしてオブジェクト指向で書いていくというのを、ドメインモデルに相当する型をたくさん書いていくというイメージ。

ドメインモデルのステータスが書きかわったというふうに書くのではなく、型が変化したという風に書くのがいいよ という主張

ただ、本だと、usecaseにdomain objectを書いているので、そのようは方法は採用していない。部分的に採用している。

  • オブジェクトの変更は「l案数適用による状態遷移」としてイミュータブルにする
  • 具体的なユースケースは"workflow"として実装
  • ドメインオブジェクトは型で構造を定義する

入力したドメインオブジェクトがワークフローにしたがって新しい状態にどんどん遷移していく。

しかし、業務のフローだと、エラーが途中で挟まるので、Result型を導入することによって管理している。

TsにはResult型がないので、NeverThrowというライブラリを用いている。

ドメインモデルの型は普通に型定義

予約の型としては、ネットからの予約と、別の形での予約のユニオンで表している。

型のコンストラクタを用意して、生成時ロジックなどを記述する

archiveCustomer = (customer: Customer): Customer => ({
  ...customer,
})
customer.archive()

という書き方から

const archived = archiveCustomer(customer)

になる。オブジェクト指向っぽい書き方では、内部状態に命令を与えることで状態を変化させる。

関数的に書くと、イミュータブルが原則なので、状態遷移後のオブジェクトを得るようにするので、状態の変化が明示的。

状態遷移が追いやすい。

上で定義した型と関数での状態遷移を、workflowとしてユースケースを実装する。

Resultを返す関数を合成して一本道の処理フローを作る。

Railway Oriented Programming

上の話の上で、他のアプリケーションの部分とどう繋げるの?

DBに保存する部分もこのように書くの?

→ そんなことない。DBの保存などの部分は普通に行なっている。

基本的に関数型スタイルでやっているのは主にドメイン層。それ以外は従来通りの作りにしている。

入力のIOとドメインロジックと出力のIOのサンドイッチ

→ これはオニオンアーキテクチャ

基本的に操作する関数と、型を同じファイルに定義する。

これは、今までDDDでやってきたこととあまり変わらない。

ではなぜこのようなことをしているのか → 型の恩恵を最大限に活用したい

型を固く定義することで、静的解析の部分でキャッチすることができるようになる。

ここまで聞くと、関数型めっちゃいいじゃんってなるけど、やはり管理が難しいのがResult型パズル。

TsはResult型をネイティブに持っていないのでこの部分が面倒な作業になる。しかし堅牢にはなる。

Haskellとかだと、モナドを用いるとそのようなことを避けることができたりする。

実際やってみてどうか

  • 2年ぐらい開発をしているが、堅牢になったと思う。不具合による障害が少ない。
  • エラーの処理漏れの不具合が少ない。ぬるぽがほぼない。普通に要件定義が間違っていたということになる。
  • unit Testが減る。型自体が保証してくれているところはテストを書かなくて良くなることがある。
  • オンボーディングがちょっと大変

感想

最近よく聞くようになったバックエンドの関数型パラダイムの導入。今まであまりイメージがかっちりとしなかったのだけれども、ちゃんとイメージが湧いた。

型によって堅牢なプログラムが書けるのがとても魅力的に感じた