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
CloudComposerによる大規模ETL 「制御と実行の分離」の実践
leveragestech
0
220
「ELT職人」から卒業!Fivetranでデータパイプラインの構築・運用から解放され、 本来の価値創造に集中できる ようになった事例
leveragestech
0
24
SpecKitでどこまでできる? コストはどれくらい?
leveragestech
1
2.7k
未来を拓くAI技術〜エージェント開発とAI駆動開発〜
leveragestech
2
270
コンテキストエンジニアリングで変わるAI活用 リファクタリングワークフローの実践から学んだ形式知
leveragestech
0
160
AirflowでDataformを制御するポイント
leveragestech
0
130
古き良き Laravel のシステムは関数型スタイルでリファクタできるのか
leveragestech
1
1.4k
リファクタリングいつやるの? 〜依存の整理〜
leveragestech
0
170
ディメンショナルモデリングを軽く語る
leveragestech
2
5.4k
Other Decks in Technology
See All in Technology
ZOZOTOWNカート決済リプレイス ── モジュラモノリスという過渡期戦略
zozotech
PRO
0
310
機密情報の漏洩を防げ! Webフロントエンド開発で意識すべき漏洩パターンとその対策
mizdra
PRO
9
3.3k
Black Hat USA 2025 Recap ~ クラウドセキュリティ編 ~
kyohmizu
0
530
コンピューティングリソース何を使えばいいの?
tomokusaba
1
160
フライトコントローラPX4の中身(制御器)を覗いてみた
santana_hammer
1
140
クレジットカードの不正を防止する技術
yutadayo
16
7.4k
Redux → Recoil → Zustand → useSyncExternalStore: 状態管理の10年とReact本来の姿
zozotech
PRO
14
7.6k
QAセントラル組織が運営する自動テストプラットフォームの課題と現状
lycorptech_jp
PRO
0
400
『HOWはWHY WHATで判断せよ』 〜『ドメイン駆動設計をはじめよう』の読了報告と、本質への探求〜
panda728
PRO
5
1.7k
こんな時代だからこそ! 想定しておきたいアクセスキー漏洩後のムーブ
takuyay0ne
4
570
CloudFormationコンソールから、実際に作られたリソースを辿れるようになろう!
amixedcolor
1
180
決済システムの信頼性を支える技術と運用の実践
ykagano
0
620
Featured
See All Featured
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
46
7.8k
How STYLIGHT went responsive
nonsquared
100
5.9k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
9.7k
Being A Developer After 40
akosma
91
590k
jQuery: Nuts, Bolts and Bling
dougneiner
65
8k
Why Our Code Smells
bkeepers
PRO
340
57k
Principles of Awesome APIs and How to Build Them.
keavy
127
17k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.3k
Balancing Empowerment & Direction
lara
5
740
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
15k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.5k
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/