関数型の考えを TypeScript に持ち込んで、テストしやすい純粋関数を増やす / Pure at the Core, Effects at the Edge: Bringing Functional Thinking into TypeScript
by
株式会社カミナシ
×
Copy
Open
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Slide 1
Slide 1 text
関数型の考えを TypeScript に持ち込んで テストしやすい純粋関数を増やす 関数型まつり2026 株式会社カミナシ Shimmy(@naoya7076)
Slide 2
Slide 2 text
経歴 ⾃⼰紹介 株式会社カミナシ Software Engineer 清⽔ 那⾳也 ヤフー株式会社でエンジニアとして開発に従事。 freee株式会社で新規プロダクト開発を⾏った後、ソフト ウェア品質の改善に携わる。 2024年 株式会社カミナシに ⼊社し既存プロダクトの開発 を⾏った後 2026年から新規プロダクトの⽴ち上げを⾏っている。
Slide 3
Slide 3 text
アジェンダ 1. 純粋関数と副作⽤ 2. TypeScriptについて、型で表現できないこと 3. FCISという考え 4. アプローチ1: Result型でエラーを値として扱う 5. アプローチ2: FCISをディレクトリ構造で表現する a. ガードレールを敷く 6. まとめ
Slide 4
Slide 4 text
ターゲット 関数型は知ってるけど TypeScript は書いてない TypeScript は書いてるけど関数型は知らない
Slide 5
Slide 5 text
第 1 部 純粋関数と副作⽤
Slide 6
Slide 6 text
⾔葉の定義 純粋関数(pure function)とは 純粋関数 => ①参照透過性 + ②副作⽤がない
Slide 7
Slide 7 text
⾔葉の定義 参照透過性とは 同じ⼊⼒に対して、常に同じ出⼒を返すこと。 ● 引数だけで結果が決まる ○ 外部の状態(グローバル変数、現在時刻など)に依存しない ● 式を戻り値に置き換えても意味が変わらない ○ f(x) をその実⾏結果の値で書き換えても、プログラム全体の 動作は変わらない
Slide 8
Slide 8 text
⾔葉の定義 副作⽤(side effect)とは 関数の内部で完結せず、関数の外の世界とやりとり する、または外の世界の影響を受けること。 ● 外部I/O:DBへの読み書き、HTTPリクエスト、ファイル操作 ● 実⾏タイミング‧回数に依存:現在時刻、乱数、ID⽣成 ● 状態変更:グローバル変数の書き換え、引数のミューテーション ● 例外を投げる
Slide 9
Slide 9 text
コード例 純粋関数 calculateReturnRate(5,100)は「いつ」「何回」読んでも 必ず5を返す。
Slide 10
Slide 10 text
コード例 副作⽤を含むコード ● 外部I/O(外部書き込み)と例外の副作⽤を持つ ● 呼ぶ度にDBを書き換える
Slide 11
Slide 11 text
1. 純粋関数(の割合)を増やす 2. 副作⽤を”分離”する 副作⽤をゼロにするのは不可能なので ⽅針
Slide 12
Slide 12 text
第2部 TypeScriptと型の限界
Slide 13
Slide 13 text
TypeScript TypeScriptという⾔語 型システムが豊かで様々な表現ができる ● JavaScriptのスーパーセット ○ JavaScriptに型を追加した ● 構造的型付け ● Union型 / 交差型 / リテラル型 etc...
Slide 14
Slide 14 text
TypeScriptの限界 限界もある 両⽅とも(x: number) => number という同じ型
Slide 15
Slide 15 text
TypeScriptの限界 TypeScriptには IO 型がない 純粋関数と副作⽤を持つ関数を 型レベルで区別できない
Slide 16
Slide 16 text
TypeScriptの限界 ⾔語 純粋関数 副作⽤を持つ関数 Haskell Int -> Int Int -> IO Int Scala A => B A => IO[B] TypeScript (x: number) => number (x: number) => number 多⾔語との⽐較
Slide 17
Slide 17 text
TypeScriptの限界 TypeScriptには モジュール境界を⾔語レベルで 強制する仕組みがない e.g. Javaのpackage
Slide 18
Slide 18 text
TypeScriptの限界 制約があるなかで 関数型の考えを TypeScriptに どのように持ち込むか
Slide 19
Slide 19 text
第3部 解決⽅法: FCISについて
Slide 20
Slide 20 text
⽅針1 TypeScriptの”⽊⽬”に沿って解決する ⽊を彫るとき、⽊⽬に逆らうと⼿間がかかる ⾔語思想(⽊⽬)に逆らわないことが⻑期的な保守性に つながる TypeScript の⾔語思想には逆らわず 副作⽤と純粋関数を分離する
Slide 21
Slide 21 text
⽅針2 FCIS(Functional Core, Imperative Shell) https://javiergonzalez.io/blog/functional-core-imperative-shell/ Functional Core: 純粋関数 Imperative Shell: I/O、副作⽤、外界 純粋関数を中に、副作⽤は端に
Slide 22
Slide 22 text
FCISとは I/O -> 純粋関数 -> I/O https://youtu.be/GYCofVVDaok?si=GWN1mbrkYtD49O4s&t=3428
Slide 23
Slide 23 text
事例 今⽇紹介する2つの事例 Result 型でエラーを 値として扱う FCISを ディレクトリ構造で実現 1 2
Slide 24
Slide 24 text
第4部 Result 型でエラーを 値として扱う
Slide 25
Slide 25 text
何が起きていたか 既存プロダクトのバリデーション ● ユーザーがフォームを定義できる ○ Google Formのようなもの ● フォームの種類でバリデーションが20種類近く ● 判定ロジック、エラー判定フラグ、 dispatch が密結合 ● 誰も⼿をつけられない状態
Slide 26
Slide 26 text
何が起きていたか
Slide 27
Slide 27 text
何が起きていたか 問題点 ● 判定ロジックと副作⽤が密結合 ● フラグ変数での状態管理 ○ isValidatedを数百⾏追い続ける ● テストにモックと巨⼤な params が必要 1つの関数の中に副作⽤と 純粋な計算が混ざり合っていた
Slide 28
Slide 28 text
アプローチ 純粋関数を増やし、副作⽤を端に追い出す 1. 「Result型」でエラーの場合も”値”で返す a. バリデーションロジックを切り出す 2. 副作⽤は端(最初や最後)に置く
Slide 29
Slide 29 text
Result型とは エラーを throw せず成功/失敗を型で表す
Slide 30
Slide 30 text
Result型とは Result型の特徴 ● エラーを throw せず、「値」として返す ● バリデーションロジックを純粋関数として書ける ● 「失敗しうる」ことが型シグネチャに現れる Result のように 成功時と失敗時の型が現れる
Slide 31
Slide 31 text
1. 純粋関数を増やせた
Slide 32
Slide 32 text
解決策2 副作⽤を端に押し出す ⼊⼒ バリデータ 失敗抽出 dispatch 結果 ● ⻘ = Functional Core:Result 型を返す純粋関数 ● ⾚ = Imperative Shell:副作⽤はここだけ
Slide 33
Slide 33 text
副作⽤は最後に 純粋関数でエラーを集め、最後に副作⽤
Slide 34
Slide 34 text
副作⽤は最後に 純粋関数でエラーを集め、最後に副作⽤
Slide 35
Slide 35 text
全体像
Slide 36
Slide 36 text
テストはどう変わったか
Slide 37
Slide 37 text
副作⽤の局在化 影響範囲の縮小 結果 得られたもの モック不要のテスト 3 バリデータが、モック不 要でユニットテスト可能 1 2 副作⽤がパイプラインの “端”にだけある 配列に1⾏⾜すだけで新し いバリデーションが増や せる 変更が簡単
Slide 38
Slide 38 text
なぜライブラリを使わなかったか TypeScriptの"⽊⽬"に沿うと ⻑期(数年単位)でメリットがある ライブラリを使う ● neverthrow, fp-ts ● 独⾃の語彙の学習コスト ○ .map / .andThen ● 依存を抱える ● TypeScriptの進化の恩恵を受 けづらい fff TypeScriptをそのまま活⽤する ● ⼈の知識に依存しない ● ライブラリに依存しない ● ⾔語の進化に乗れる
Slide 39
Slide 39 text
第5部 FCIS をディレクトリ構造 で実現する
Slide 40
Slide 40 text
How なぜディレクトリで分けるのか ● TypeScriptにはIO型がない ● モジュール境界を⾔語レベルで強制する仕組みもない ○ e.g. Javaのpackage ディレクトリ構造で 副作⽤が⼊らない場所と⼊る場所を分ける
Slide 41
Slide 41 text
How 配置と依存で守る 1. ディレクトリの配置を決める a. domain/ infrastructure/ usecase/ という3層構造 2. ディレクトリ間の依存関係を制限する a. domain/ から infrastructure/ や usecase/ への importを禁⽌ 3. domain層で呼べるものも制限 a. I/Oライブラリなどの呼び出しも制限
Slide 42
Slide 42 text
全体像 配置と依存で守る
Slide 43
Slide 43 text
メリット この構造で得られるもの 1 モック不要のテスト 2 副作⽤の局在化 2 読みやすさ domain 層は 純粋関数だけになる 何をするところななのか がディレクトリで⼀⽬で わかる domain 層は 純粋関数だけになる
Slide 44
Slide 44 text
第5-a部 境界を強制する
Slide 45
Slide 45 text
● ディレクトリで分けただけでは意味がない ● TypeScriptコンパイラは何も⾔わない ● ⼈間のレビューで守る? ● AIがコードが書く時は? ➡ 機械にチェックさせる なぜ強制するのか
Slide 46
Slide 46 text
How dependency-cruiser ファイルから別のファイルへのimportを パスのパターンで許可‧禁⽌するツール アーキテクチャのルールを 設定ファイルとしてコードでかける https://github.com/sverweij/dependency-cruiser
Slide 47
Slide 47 text
dependency-cruiserの設定例 domain/ から infrastructure/ や usecase/ へのimportを禁⽌
Slide 48
Slide 48 text
違反したら何が起きるか どのファイルが、どのルールに違反して どこに依存しているかが⼀⽬でわかる。
Slide 49
Slide 49 text
Lefthookで強制する pre-commit, pushで必ず動くようにする https://github.com/evilmartians/lefthook
Slide 50
Slide 50 text
第6部 まとめ
Slide 51
Slide 51 text
2つのアプローチ アプローチ1 アプローチ2 背景 既存プロダクト 新規プロダクト 範囲 関数、パイプライン プロジェクト全体 アプローチ Result 型でエラーを値に ディレクトリで 層を分ける
Slide 52
Slide 52 text
まとめ FCIS: 純粋関数を中に、副作⽤は端に TypeScriptの⽊⽬に沿って3つの⽅法で実現した 1. 型で守る … Result 型でエラーを値に 2. 配置で守る … domain / infrastructure / server 3. 機械で守る … dependency-cruiser × pre-commit
Slide 53
Slide 53 text
株式会社カミナシ https://kaminashi.jp