Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで
Search
Tech Leverages
May 13, 2024
Technology
2
16k
Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで
TSKaigi2024で、テックリードの竹下が発表した
セッション
のスライドです。
※竹下は、一般社団法人TSKaigi Asscosiationの代表理事も兼任しております
Tech Leverages
May 13, 2024
Tweet
Share
More Decks by Tech Leverages
See All by Tech Leverages
未来を拓くAI技術〜エージェント開発とAI駆動開発〜
leveragestech
2
220
コンテキストエンジニアリングで変わるAI活用 リファクタリングワークフローの実践から学んだ形式知
leveragestech
0
120
AirflowでDataformを制御するポイント
leveragestech
0
110
古き良き Laravel のシステムは関数型スタイルでリファクタできるのか
leveragestech
1
1.2k
リファクタリングいつやるの? 〜依存の整理〜
leveragestech
0
120
ディメンショナルモデリングを軽く語る
leveragestech
1
5.1k
アクターモデルによる効率的な分散システム設計
leveragestech
0
5k
Terraform による運用効率化の取り組みと最新のテストアプローチの紹介
leveragestech
0
4.9k
OpenFGAで拓く次世代認可基盤 〜予告編〜
leveragestech
0
5k
Other Decks in Technology
See All in Technology
MagicPod導入から半年、オープンロジQAチームで実際にやったこと
tjoko
0
110
プラットフォーム転換期におけるGitHub Copilot活用〜Coding agentがそれを加速するか〜 / Leveraging GitHub Copilot During Platform Transition Periods
aeonpeople
1
240
品質視点から考える組織デザイン/Organizational Design from Quality
mii3king
0
210
「Linux」という言葉が指すもの
sat
PRO
4
140
【NoMapsTECH 2025】AI Edge Computing Workshop
akit37
0
230
エンジニアが主導できる組織づくり ー 製品と事業を進化させる体制へのシフト
ueokande
1
100
TS-S205_昨年対比2倍以上の機能追加を実現するデータ基盤プロジェクトでのAI活用について
kaz3284
1
230
スクラムガイドに載っていないスクラムのはじめかた - チームでスクラムをはじめるときに知っておきたい勘所を集めてみました! - / How to start Scrum that is not written in the Scrum Guide 2nd
takaking22
1
190
OCI Oracle Database Services新機能アップデート(2025/06-2025/08)
oracle4engineer
PRO
0
180
複数サービスを支えるマルチテナント型Batch MLプラットフォーム
lycorptech_jp
PRO
1
960
新規プロダクトでプロトタイプから正式リリースまでNext.jsで開発したリアル
kawanoriku0
1
210
Aurora DSQLはサーバーレスアーキテクチャの常識を変えるのか
iwatatomoya
1
1.2k
Featured
See All Featured
Building Flexible Design Systems
yeseniaperezcruz
329
39k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
50k
Bootstrapping a Software Product
garrettdimon
PRO
307
110k
GraphQLとの向き合い方2022年版
quramy
49
14k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.7k
Automating Front-end Workflow
addyosmani
1370
200k
Statistics for Hackers
jakevdp
799
220k
Scaling GitHub
holman
463
140k
Building Applications with DynamoDB
mza
96
6.6k
YesSQL, Process and Tooling at Scale
rocio
173
14k
Stop Working from a Prison Cell
hatefulcrawdad
271
21k
A Modern Web Designer's Workflow
chriscoyier
696
190k
Transcript
Confidential © 2023 Leverages Co., Ltd. Step by Stepで学ぶ、ADT(代数的データ 型)、モナドからEffect-TSまで
2024/5/11 テクノロジー戦略室 室長 竹下 義晃
© 2023 Leverages Co., Ltd. レバレジーズ株式会社 竹下 義晃 テクノロジー戦略室室長 一般社団法人TSKaigi
Association 代表理事、一般社団法人 Japan Scala Association理事 2009年に東京大学大学院農学生命科学科を修了 芸者東京 株式会社で、アプリen、バックエンドen、フロントen、アプリマーケターを経て 2020年にレバレジーズに入社 フルスタックの技術力を背景に、レバレジーズ社の技術の向上とエンジニア組織文化の構築に取り組む。また、 ScalaMatsuriやTSKaigiの運営にも関わり、技術コミュニティを盛り上げる活動も行っている。
© 2023 Leverages Co., Ltd. 関数型と聞いて どんな印象がありますか? 3
© 2023 Leverages Co., Ltd. 関数型ってこんなもんなのか • すでに身近に使っている ◦ Arrayのfilter,
map, flatMap, reduce • 関数型の概念も、根底にはなんらかの課題があり、それを 解決している • 私はJava,C# => Scalaで関数型に入門しました 4
© 2023 Leverages Co., Ltd. 目次 1. このセッションでの説明範囲 2. 今日のサンプルコード
3. 代数的データ型: リッチな型表現 4. Either: 成功/失敗を型で表現 5. TaskEither: 非同期処理も巻き込む 6. Effect-TS: すべてを一つに 5
© 2023 Leverages Co., Ltd. このセッションでの説明範囲 01 6
© 2023 Leverages Co., Ltd. 説明すること • ADT(代数的データ型), Either(Result型), TaskEither,
Effect-tsを、型の表 現力に焦点を当ててStep by Stepで説明していきます 7 説明しないこと • 関数型言語や、モナドの理論的な話 • Effect-tsのCollection MonadやIO Monadなどの側面 • 各ライブラリ(fp-tsやEffect-TS)の詳しい使い方 • コードや設計自体の良し悪し
© 2023 Leverages Co., Ltd. 今日のサンプルコード 02 8
© 2023 Leverages Co., Ltd. ユーザー登録 9 e-mailとpasswordで、ユーザー登録を行い、完了メールを送る // 入力値のチェック
function validateInput( param: { email: string, password: string}): boolean // DBにレコードを保存 function save(param: {email: string, password: string}): { id: string} // 登録完了メールの送信 function sendRegisteredEmail(email: string): void
© 2023 Leverages Co., Ltd. 普通のコード 10 function createUser( param:
{email: string, password: string}): {id: string} { if( !validateInput(param)) { throw new Error("バリデーションエラー ") } let result: { id: string } = { id: "" }; try { result = save( param ); } catch { throw new Error("すでに登録済みのemailです。") } sendRegisteredEmail(param.email); return result }
© 2023 Leverages Co., Ltd. 普通のコード 11 function createUser( param:
{email: string, password: string}): {id: string} { if( !validateInput(param)) { throw new Error("バリデーションエラー ") } let result: { id: string } = { id: "" }; try { result = save( param ); } catch { throw new Error("すでに登録済みのemailです。") } sendRegisteredEmail(param.email); return result } 戻り値がboolの場合、true/falseどちらが成功パ ターンになるか人依存 どんなエラーが投げられるかよくわからない
© 2023 Leverages Co., Ltd. 問題点 12 • ソースコードを読まないと関数の挙動がわからな い
◦ 成功時はUserIDが帰って来そうだが、エラー 時は? • ifとかtryとか入り乱れる
© 2023 Leverages Co., Ltd. 代数的データ型 03 13
© 2023 Leverages Co., Ltd. 代数的データ型(ADT)とは 14 • 直積型と、直和型を組み合わせて表現される型 直積
type User = {name : string, age: number} = nameとageの直積型 直和 type SNS = “Google” | “Facebook” | “X”
© 2023 Leverages Co., Ltd. ADTの導入 15 function validateInput(param: {
email: string; password: string; }): | "Success" | { type: "InvalidEmail" ; message: string } | { type: "InvalidPassword" ; message: string } function save(param: { email: string; password: string; }): | { id: string } | { type: "DuplicateEmail" ; message: string } function sendRegisteredEmail(email: string): void {}
© 2023 Leverages Co., Ltd. ADTの導入 16 function validateInput(param: {
email: string; password: string; }): | "Success" | { type: "InvalidEmail" ; message: string } | { type: "InvalidPassword" ; message: string } function save(param: { email: string; password: string; }): | { id: string } | { type: "DuplicateEmail" ; message: string } function sendRegisteredEmail(email: string): void {} どんなエラーが返ってくるかまで、表現できるよう になった
© 2023 Leverages Co., Ltd. ADTの導入 17 function createUser(param: {
email: string; password: string }): | { id: string } | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string } { const validationResult = validateInput(param); if (validationResult !== "Success") { return { type: "ValidationError", field: validationResult.type === "InvalidEmail" ? "email" : "password", message: validationResult.message, }; } const saveResult = save(param); if (saveResult.type === "AlreadyRegistered") { return { type: "AlreadyRegistered", message: saveResult.message, }; } sendRegisteredEmail(param.email); return saveResult; }
© 2023 Leverages Co., Ltd. ADTの導入 18 function createUser(param: {
email: string; password: string }): | { id: string } | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string } { const validationResult = validateInput(param); if (validationResult !== "Success") { return { type: "ValidationError", field: validationResult.type === "InvalidEmail" ? "email" : "password", message: validationResult.message, }; } const saveResult = save(param); if (saveResult.type === "AlreadyRegistered") { return { type: "AlreadyRegistered", message: saveResult.message, }; } sendRegisteredEmail(param.email); return saveResult; } 別々のフィールドをもたせられるようにもなってい る
© 2023 Leverages Co., Ltd. 改善点と問題点 19 改善点 • 型定義を見るだけでなんとメソッドの挙動がわか
る ◦ = 宣言的になった 問題点 • 依然としてif文多い ◦ 結果のチェックミスによるバグが発生する
© 2023 Leverages Co., Ltd. Either 04 20
© 2023 Leverages Co., Ltd. Eitherとは 21 • Either<Left, Right>の型定義を持つMonad
◦ Right=正しいので、成功の型を表す • Result<Success>や、Result<Success, Failure>と同じ 使われ方 成功したか、失敗したかを型で表現できるようにしたもの
© 2023 Leverages Co., Ltd. Eitherの導入 22 import { Either,
left, right } from "fp-ts/lib/Either" ; function validateInput(param: { email: string; password: string; }): Either< | { type: "InvalidEmail" ; message: string } | { type: "InvalidPassword" ; message: string }, void > function save(param: { email: string; password: string; }): Either<{ type: "AlreadyRegistered" ; message: string }, { id: string }> function sendRegisteredEmail(email: string): void
© 2023 Leverages Co., Ltd. Eitherの導入 23 function createUser(param: {
email: string; password: string }): Either< | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string }, { id: string } > { const validationResult = validateInput(param); if (isLeft(validationResult)) { return left({ type: "ValidationError", field: validationResult.left.type === "InvalidEmail" ? "email" : "password", message: validationResult.left.message, }); } const saveResult = save(param); if (isLeft(saveResult)) { return left({ type: "AlreadyRegistered", message: saveResult.left.message, }); } sendRegisteredEmail(param.email); return saveResult; }
© 2023 Leverages Co., Ltd. Eitherの導入 24 function createUser(param: {
email: string; password: string }): Either< | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string }, { id: string } > { const validationResult = validateInput(param); if (isLeft(validationResult)) { return left({ type: "ValidationError", field: validationResult.left.type === "InvalidEmail" ? "email" : "password", message: validationResult.left.message, }); } const saveResult = save(param); if (isLeft(saveResult)) { return left({ type: "AlreadyRegistered", message: saveResult.left.message, }); } sendRegisteredEmail(param.email); return saveResult; } 成功/失敗はisRight/isLeftで判定すれば良い
© 2023 Leverages Co., Ltd. 改善点と問題点 25 改善点 • 型で成功失敗までわかるようになった
• 条件判定も画一化した 問題点 • 依然としてif文多い • むしろ冗長になっている
© 2023 Leverages Co., Ltd. pipeの導入 26 import { Either,
left, right, flatMap, tap, mapLeft } from "fp-ts/lib/Either"; import { pipe } from "fp-ts/function"; function createUser(param: { email: string; password: string }) { const convertValidationError = (error: { type: "InvalidEmail" | "InvalidPassword"; message: string; }) => (...) const convertSaveError = (error: { message: string }) => (...) return pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError) )), tap(() => sendRegisteredEmail(param.email)) ); }
© 2023 Leverages Co., Ltd. pipeの導入(型の確認) 27 type CreateUserValidationError =
{ type: "ValidationError"; field: string; message: string } type ValidateInputError = | { type: "InvalidEmail"; message: string } | { type: "InvalidPassword"; message: string }
© 2023 Leverages Co., Ltd. pipeの導入 28 import { Either,
left, right, flatMap, tap, mapLeft } from "fp-ts/lib/Either"; import { pipe } from "fp-ts/function"; function createUser(param: { email: string; password: string }) { const convertValidationError = (error: { type: "InvalidEmail" | "InvalidPassword"; message: string; }) => (...) const convertSaveError = (error: { message: string }) => (...) return pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError) )), tap(() => sendRegisteredEmail(param.email)) ); } エラー型を戻り値の型に変換する処理として分離
© 2023 Leverages Co., Ltd. 改善点と問題点 29 改善点 • if文がなくなった
◦ データフローとして表現できるように • 処理を分割して、組み合わせやすくなった 問題点 • pipeの書き方/読み方に慣れる必要ある ◦ 型パズルの一端 • あれ?Promiseは? ◦ saveとか普通Promiseだよね?
© 2023 Leverages Co., Ltd. TaskEither 05 30
© 2023 Leverages Co., Ltd. Promise<Either<L,R>>だと? 31 async function add(a:
number, b: number): Promise<Either<string, number>> { return right(a + b); } async function div(a: number, b: number): Promise<Either<string, number>> { return right(a / b); } async function main() { pipe( await add(1, 2), // Compile Error! // Type 'Promise<Either<string, number>>' is not assignable // to type 'Either<unknown, unknown>' // flatMapが非同期関数を受け取れない flatMap(async (result) => await div(result, 0)) ) }
© 2023 Leverages Co., Ltd. TaskとTaskEither 32 • Task<T>は、”処理”を型で表現したMonad ◦
実態は() => Promise<T> ◦ Taskは作るだけではまだ実行されていない ◦ 作ってrunしたタイミングで初めて処理が実行 される • TaskEither<L,R>はTaskとEitherをMonad Transformerで合成した、新しいMonad
© 2023 Leverages Co., Ltd. TaskEitherの導入 33 import { TaskEither,
left, right } from "fp-ts/lib/TaskEither" ; function validateInput(param: { email: string; password: string; }): TaskEither< | { type: "InvalidEmail" ; message: string } | { type: "InvalidPassword" ; message: string }, void > function save(param: { email: string; password: string; }): TaskEither<{ type: "AlreadyRegistered" ; message: string }, { id: string }> async function sendRegisteredEmail(email: string): Task<void> {} EitherがTaskEitherに置き換わっただけ
© 2023 Leverages Co., Ltd. TaskEitherの導入 34 import { ...,
fromTask } from "fp-ts/lib/TaskEither"; import { Either } from "fp-ts/lib/Either"; import { pipe } from "fp-ts/function"; function createUser(param: { email: string; password: string }): Promise<Either< | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string }, { id: string } >> { ... const program = pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError))), tap(() => fromTask(sendRegisteredEmail(param.email))) ); return program();
© 2023 Leverages Co., Ltd. TaskEitherの導入 35 import { ...,
fromTask } from "fp-ts/lib/TaskEither"; import { Either } from "fp-ts/lib/Either"; import { pipe } from "fp-ts/function"; function createUser(param: { email: string; password: string }): Promise<Either< | { type: "ValidationError"; field: string; message: string } | { type: "AlreadyRegistered"; message: string }, { id: string } >> { ... const program = pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError))), tap(() => fromTask(sendRegisteredEmail(param.email))) ); return program(); Task => TaskEitherへの変換が発生
© 2023 Leverages Co., Ltd. 改善点と問題点 36 改善点 • PromiseもMonadに巻き込めた
問題点 • 型パズル感UP ◦ TaskをTaskEitherへ変換が必要
© 2023 Leverages Co., Ltd. Effect-TSの導入 06 37
© 2023 Leverages Co., Ltd. • Effect Monadを中心としたライブラリ ◦ 今回はTaskEitherの範囲での紹介
◦ Effect自体にはもっと豊富な機能があります • ScalaのZIOをTypeScriptにポートしてきたライブラリ Effect-TS 38
© 2023 Leverages Co., Ltd. • Effect<Success, Error = unknown,
Requirements = unknown> の型を持つ • Requirementsは今回触れませんが、副作用の分離にも 使えます Effect 39
© 2023 Leverages Co., Ltd. Effectの導入 40 import { Effect
} from "effect"; function validateInput(param: { email: string; password: string; }): Effect.Effect< void, | { type: "InvalidEmail"; message: string } | { type: "InvalidPassword"; message: string } > function save(param: { email: string; password: string; }): Effect.Effect< { id: string }, { type: "AlreadyRegistered"; message: string } > function sendRegisteredEmail(email: string): Effect.Effect<void> TaskEitherがEffectに置き換わっただけ
© 2023 Leverages Co., Ltd. Effectの導入 41 import { Effect,
pipe} from "effect"; function createUser(param: { email: string; password: string }): Promise<{ id: string; }> { ... const program = pipe( pipe( validateInput(param), Effect.mapError(convertValidationError)), Effect.flatMap(() => pipe( save(param), Effect.mapError(convertSaveError))), Effect.tap(() => sendRegisteredEmail(param.email)) ); return Effect.runPromise(program); }
© 2023 Leverages Co., Ltd. Effect generatorスタイル 42 import {
Effect, pipe} from "effect"; function createUser(param: { email: string; password: string }): Promise<{ id: string; }> { ... const program = Effect.gen(function* (_) { const validationResult = yield* _( pipe(validateInput(param), Effect.mapError(convertValidationError)) ); const saveResult = yield* _( pipe(save(param), Effect.mapError(convertSaveError)) ); yield* _(sendRegisteredEmail(param.email)); return saveResult; }); return Effect.runPromise(program); }
© 2023 Leverages Co., Ltd. まとめ 43
© 2023 Leverages Co., Ltd. 1. ADTで型の表現力上げたい 2. 成功/失敗などまで型で表現してバグを減らしたい 3.
Promiseも含めて一緒くたに扱いたい 4. そうだ、Effect-TS使おう 流れ 44
© 2023 Leverages Co., Ltd. • 今回は、Step by Stepのためfp-tsを使ってモナド周りを説 明しました
• 実践ではEffect-TSだけで機能は実現できます Effect-TSだけ使えばOK 45
© 2023 Leverages Co., Ltd. • バグが減る ◦ ロジックまで型で表現できて、コンパイラがチェックして くれる
• 拡張性、メンテナンス性UP ◦ 処理をより小さく分割し易い ◦ データフローの定義になり、より宣言的なコードになる 効能 46
© 2023 Leverages Co., Ltd. 是非、Effect-TSを使って より良いサービスを作ってください 47
© 2023 Leverages Co., Ltd. We are hiring! 40を超えるサービスを、 すべてオールインハウスで開発!
現在レバレジーズでは一緒に働いてくれる仲間を募集しています。 もし、ご興味ある方は、以下のリンクからご応募ください。 https://recruit.leverages.jp/recruit/engineer/