Slide 1

Slide 1 text

マイクロサービス内で 動くAPIを F#で書いている 2025/06/15 関数型まつり

Slide 2

Slide 2 text

あやぴー - 株式会社ユーザベース - 9LISPという勉強会で道を外したヒト - 約10年関数型言語を業務で使っている - 最近はよくClojure, F#を書いてます - @ayato-p - #fp_matsuri_c

Slide 3

Slide 3 text

どんな開発組織でF#を採用したのか

Slide 4

Slide 4 text

プロダクトチーム紹介 - XPに基づいたアジャイル開発組織 - 100人超の開発者(SWE, SRE, MLE, TE)が在籍 - ペアプログラミングを中心とした開発 - チームの最大人数は5人 - 規模が大きくなってもチームで独立して動けるようにする - 自律したチームなので明示的なリーダーは不在 - マイクロサービスとしてサービスを開発している - モノリス -> マイクロサービスをやってきている - 新規機能はマイクロサービスで開発している - モノリス部分は随時リプレース - マイクロサービスの各APIは多種多様な言語で書かれている - ↑大事

Slide 5

Slide 5 text

プロダクトチーム紹介 ※ イメージです

Slide 6

Slide 6 text

https://agilejourney.uzabase.com/archive/category/UZABASE 興味があれば…

Slide 7

Slide 7 text

F#導入以前のプロダクトチーム - Scala, Kotlin, ClojureなどJVM系言語を筆頭に様々な言語が採用されていた - Elixir, Go, Rustなどの時勢に乗った言語の採用もされていた - OCaml, Haskellのようなちょっとレアな言語も採用されていた - フロントエンドではDart, TypeScriptなども採用されていた - 何故か.NET系の言語だけは採用されていなかった

Slide 8

Slide 8 text

どのように導入したのか - 当時私がいたチームをそそのかした - 全員ノリがよかった - Core APIにデータを投入するAPI ※ イメージです

Slide 9

Slide 9 text

なぜF#だったのか - 組織内で誰も使ったことがなかった - 静的型付けな関数型言語で現実世界で使いやすそう - シンタックスが簡素に感じた - エコシステムは.NETのものが流用できそう

Slide 10

Slide 10 text

開発環境 - .NET (Core) - クロスプラットフォームで動く、モダン.NET - dotnetコマンド - ビルドなど - Paket - 依存関係の管理 - Ionide - VS Code / Cursorで動作するプラグイン - Fantomas - フォーマッター

Slide 11

Slide 11 text

F#らしさを活かした設計

Slide 12

Slide 12 text

F#らしさ - 第一級関数 - コンピュテーション式 - 判別共用体 - 例外 現実世界の問題を解くのにちょうどいい。

Slide 13

Slide 13 text

ASP.NET Core Minimal APIのスタイルを採用した - 関数プログラミングと親和性が高い - フレームワークよりライブラリの組み合わせが好ましいと考えていた - Library patterns: Why frameworks are evil - Tomas Petricek - プロダクトチームとしてはフレームワークに求めている機能が多くはない - 外界を適切に分離して、ドメインを中心に据えたアーキテクチャを好むため ※ ルーティングに対して関数を登録するだけでよく、従来のフレームワークのようにクラスを作ったりアトリビュートを大量に付与する必要がない

Slide 14

Slide 14 text

アーキテクチャ概観 ※ こういうアーキテクチャで開発することが多い

Slide 15

Slide 15 text

境界にインターフェイスではなく関数型を使う - 関数型を境界に用いると他の関数と組み合わせるのが容易になる - 依存先をテストするさいにモックしやすい

Slide 16

Slide 16 text

ドメインを素朴に保つ - ドメインにある関数やレコードはできるだけモナドや副作用と無縁にする - 特に Option はEffective Javaでも言及があるようにフィールドに保持しない - F#の場合は判別共用体で宣言できる可能性が高い

Slide 17

Slide 17 text

ドメインを素朴に保つ - モデリング次第ではあるが判別共用体を頼る方が意味がハッキリする - Option を使うより良い可能性が高い

Slide 18

Slide 18 text

ユースケースで抽象度をあげてエラーを扱う - 判別共用体を多用すると具体的でハンドリングしにくい - 鉄道指向プログラミングを目指したい ※ このくらいなら許容できるレベルではあるが…

Slide 19

Slide 19 text

ユースケースで抽象度をあげてエラーを扱う

Slide 20

Slide 20 text

Resultに頼りすぎず例外を適切に使う - 関数プログラミングの書籍では例外より Result 型の良さが強調されがち - 現実世界では例外で済む事情をわざわざ Result 型に変換する必要はない - 特に外界の事情や業務上のエラーではない状態を Result 型に押し込めてはいけない ※ タイムアウトをResult型や判別共用体に変換してまでドメインレイヤーまで持ち上げたくないよね

Slide 21

Slide 21 text

非同期の取り扱い

Slide 22

Slide 22 text

ドメインは素朴にしたいが…Asyncだけは特別扱い - 以下のように Async を返す関数を引数に取る関数を定義したいことがある - これを無理に消すと Async.RunSynchronously などを使うことになる - DDDで集約を跨いだ情報でロジックを構築するための「getter高階関数パターン」の紹介 - F#で Async を無理に外そうとすると大変な目に…(後述)

Slide 23

Slide 23 text

Asyncを不用意に止めない - Async.RunSynchronously を使って非同期計算を止めてしまうのは禁忌 - .NETは賢いのでスレッドプールを拡大し続けるが、そのために気付きにくい問題になる - レスポンスの値は基本的に Task やそれに準ずるものにしておく - F#の Async は返り値に指定できないので注意 ※ 上記のようなコードを書いてはいけない

Slide 24

Slide 24 text

Asyncを不用意に止めない - 返り値は必ず Task にする

Slide 25

Slide 25 text

TaskよりAsyncを使う - Task は即時実行される、 Async は遅延実行される - 計算の遅延と仲良く付き合う

Slide 26

Slide 26 text

並行処理で想像以上にリクエストが飛ぶ - Async.Parallel で気軽に並行処理が実装できる - 問答無用で通信先を◯しにいく

Slide 27

Slide 27 text

並行処理で想像以上にリクエストが飛ぶ - その先がHTTP通信ならHTTP Clientの同時接続数設定をいれておく - NET 8 Networking Improvements - Async.Parallel の maxDegreeOfParallelism を設定するのも良い - FSharp.Core - Async

Slide 28

Slide 28 text

その他、実装上の悩みごと

Slide 29

Slide 29 text

Minimal APIはF#からの利用に最適化されていない① - C#ではラムダ式を素直に受け取れるようになっている - F#の匿名関数はラムダ式と直接の互換性がないので Func に変換する - Func<> and Delegate implicit conversion · Issue #1131 · fsharp/fslang-suggestions · GitHub

Slide 30

Slide 30 text

Minimal APIはF#からの利用に最適化されていない② - パラメーターバインドで苦労する - 匿名関数にアトリビュートを使えないなど問題が多々ある - Support attributes on lambda expressions · Issue #984 · fsharp/fslang-suggestions · GitHub

Slide 31

Slide 31 text

Minimal APIはF#からの利用に最適化されていない② - パラメーターバインドで取れないときは BindAsync を自前で実装する

Slide 32

Slide 32 text

コンピュテーション式の取り扱いが増えると煩雑 - FSharpPlusを使えるとコンピュテーション式の利用を減らせる - FSharpPlus自体は便利だが… - ドキュメントはあまり親切ではない - monadコンピュテーション式など使えるのは良い

Slide 33

Slide 33 text

DIにReaderを使うか…?

Slide 34

Slide 34 text

関数の呼び出しを検証するのが面倒 - 自分で関数を用意する必要がある

Slide 35

Slide 35 text

運用は大変か?

Slide 36

Slide 36 text

大変ではない - F#特有の難しさはあまりない - .NETに慣れていないことで発生している難しさはある - 逆に.NETの運用に慣れていれば簡単に運用できるはず - dotnet-countersのようなツールを知っていれば◎ - KubernetesにデプロイしているのでDockerイメージをビルドできれば◎ - 諸々のサービス利用も容易◎ - Datadogでメトリクス収集をしている - yamoryでのスキャンも簡単

Slide 37

Slide 37 text

強いて言うなら… - 日本語の情報は例によって少ない - とはいえ、私達の組織にとっては誤差でしかない - そういう言語をよく使っている - .NETやC#に慣れていないことが原因の大変さはある - ドキュメントの読み替えや理解がどうしても難しい - 拡張関数とかどのパッケージ開いたらいいの?とかありがち - 生成AIも万能ではない

Slide 38

Slide 38 text

まとめ

Slide 39

Slide 39 text

まとめ - F#らしさを活かした設計をする - ドメインは素朴な関数やレコードを活用 - 一部で抽象度をあげてエラーハンドリングをする - 例外もちゃんと使う - 非同期はAsyncを適切に利用する - FSharpPlusはすごいが扱うのが難しい - .NETの運用は実務上困ることはない…はず