Slide 1

Slide 1 text

Confidential © 2023 Leverages Co., Ltd. Step by Stepで学ぶ、ADT(代数的データ 型)、モナドからEffect-TSまで 2024/5/11 テクノロジー戦略室 室長 竹下 義晃

Slide 2

Slide 2 text

© 2023 Leverages Co., Ltd. レバレジーズ株式会社 竹下 義晃 テクノロジー戦略室室長 一般社団法人TSKaigi Association 代表理事、一般社団法人 Japan Scala Association理事 2009年に東京大学大学院農学生命科学科を修了 芸者東京 株式会社で、アプリen、バックエンドen、フロントen、アプリマーケターを経て 2020年にレバレジーズに入社 フルスタックの技術力を背景に、レバレジーズ社の技術の向上とエンジニア組織文化の構築に取り組む。また、 ScalaMatsuriやTSKaigiの運営にも関わり、技術コミュニティを盛り上げる活動も行っている。

Slide 3

Slide 3 text

© 2023 Leverages Co., Ltd. 関数型と聞いて どんな印象がありますか? 3

Slide 4

Slide 4 text

© 2023 Leverages Co., Ltd. 関数型ってこんなもんなのか ● すでに身近に使っている ○ Arrayのfilter, map, flatMap, reduce ● 関数型の概念も、根底にはなんらかの課題があり、それを 解決している ● 私はJava,C# => Scalaで関数型に入門しました 4

Slide 5

Slide 5 text

© 2023 Leverages Co., Ltd. 目次 1. このセッションでの説明範囲 2. 今日のサンプルコード 3. 代数的データ型: リッチな型表現 4. Either: 成功/失敗を型で表現 5. TaskEither: 非同期処理も巻き込む 6. Effect-TS: すべてを一つに 5

Slide 6

Slide 6 text

© 2023 Leverages Co., Ltd. このセッションでの説明範囲 01 6

Slide 7

Slide 7 text

© 2023 Leverages Co., Ltd. 説明すること ● ADT(代数的データ型), Either(Result型), TaskEither, Effect-tsを、型の表 現力に焦点を当ててStep by Stepで説明していきます 7 説明しないこと ● 関数型言語や、モナドの理論的な話 ● Effect-tsのCollection MonadやIO Monadなどの側面 ● 各ライブラリ(fp-tsやEffect-TS)の詳しい使い方 ● コードや設計自体の良し悪し

Slide 8

Slide 8 text

© 2023 Leverages Co., Ltd. 今日のサンプルコード 02 8

Slide 9

Slide 9 text

© 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

Slide 10

Slide 10 text

© 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 }

Slide 11

Slide 11 text

© 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どちらが成功パ ターンになるか人依存 どんなエラーが投げられるかよくわからない

Slide 12

Slide 12 text

© 2023 Leverages Co., Ltd. 問題点 12 ● ソースコードを読まないと関数の挙動がわからな い ○ 成功時はUserIDが帰って来そうだが、エラー 時は? ● ifとかtryとか入り乱れる

Slide 13

Slide 13 text

© 2023 Leverages Co., Ltd. 代数的データ型 03 13

Slide 14

Slide 14 text

© 2023 Leverages Co., Ltd. 代数的データ型(ADT)とは 14 ● 直積型と、直和型を組み合わせて表現される型 直積 type User = {name : string, age: number} = nameとageの直積型 直和 type SNS = “Google” | “Facebook” | “X”

Slide 15

Slide 15 text

© 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 {}

Slide 16

Slide 16 text

© 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 {} どんなエラーが返ってくるかまで、表現できるよう になった

Slide 17

Slide 17 text

© 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; }

Slide 18

Slide 18 text

© 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; } 別々のフィールドをもたせられるようにもなってい る

Slide 19

Slide 19 text

© 2023 Leverages Co., Ltd. 改善点と問題点 19 改善点 ● 型定義を見るだけでなんとメソッドの挙動がわか る ○ = 宣言的になった 問題点 ● 依然としてif文多い ○ 結果のチェックミスによるバグが発生する

Slide 20

Slide 20 text

© 2023 Leverages Co., Ltd. Either 04 20

Slide 21

Slide 21 text

© 2023 Leverages Co., Ltd. Eitherとは 21 ● Eitherの型定義を持つMonad ○ Right=正しいので、成功の型を表す ● Resultや、Resultと同じ 使われ方 成功したか、失敗したかを型で表現できるようにしたもの

Slide 22

Slide 22 text

© 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

Slide 23

Slide 23 text

© 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; }

Slide 24

Slide 24 text

© 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で判定すれば良い

Slide 25

Slide 25 text

© 2023 Leverages Co., Ltd. 改善点と問題点 25 改善点 ● 型で成功失敗までわかるようになった ● 条件判定も画一化した 問題点 ● 依然としてif文多い ● むしろ冗長になっている

Slide 26

Slide 26 text

© 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)) ); }

Slide 27

Slide 27 text

© 2023 Leverages Co., Ltd. pipeの導入(型の確認) 27 type CreateUserValidationError = { type: "ValidationError"; field: string; message: string } type ValidateInputError = | { type: "InvalidEmail"; message: string } | { type: "InvalidPassword"; message: string }

Slide 28

Slide 28 text

© 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)) ); } エラー型を戻り値の型に変換する処理として分離

Slide 29

Slide 29 text

© 2023 Leverages Co., Ltd. 改善点と問題点 29 改善点 ● if文がなくなった ○ データフローとして表現できるように ● 処理を分割して、組み合わせやすくなった 問題点 ● pipeの書き方/読み方に慣れる必要ある ○ 型パズルの一端 ● あれ?Promiseは? ○ saveとか普通Promiseだよね?

Slide 30

Slide 30 text

© 2023 Leverages Co., Ltd. TaskEither 05 30

Slide 31

Slide 31 text

© 2023 Leverages Co., Ltd. Promise>だと? 31 async function add(a: number, b: number): Promise> { return right(a + b); } async function div(a: number, b: number): Promise> { return right(a / b); } async function main() { pipe( await add(1, 2), // Compile Error! // Type 'Promise>' is not assignable // to type 'Either' // flatMapが非同期関数を受け取れない flatMap(async (result) => await div(result, 0)) ) }

Slide 32

Slide 32 text

© 2023 Leverages Co., Ltd. TaskとTaskEither 32 ● Taskは、”処理”を型で表現したMonad ○ 実態は() => Promise ○ Taskは作るだけではまだ実行されていない ○ 作ってrunしたタイミングで初めて処理が実行 される ● TaskEitherはTaskとEitherをMonad Transformerで合成した、新しいMonad

Slide 33

Slide 33 text

© 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 {} EitherがTaskEitherに置き換わっただけ

Slide 34

Slide 34 text

© 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> { ... const program = pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError))), tap(() => fromTask(sendRegisteredEmail(param.email))) ); return program();

Slide 35

Slide 35 text

© 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> { ... const program = pipe( pipe( validateInput(param), mapLeft(convertValidationError)), flatMap(() => pipe( save(param), mapLeft(convertSaveError))), tap(() => fromTask(sendRegisteredEmail(param.email))) ); return program(); Task => TaskEitherへの変換が発生

Slide 36

Slide 36 text

© 2023 Leverages Co., Ltd. 改善点と問題点 36 改善点 ● PromiseもMonadに巻き込めた 問題点 ● 型パズル感UP ○ TaskをTaskEitherへ変換が必要

Slide 37

Slide 37 text

© 2023 Leverages Co., Ltd. Effect-TSの導入 06 37

Slide 38

Slide 38 text

© 2023 Leverages Co., Ltd. ● Effect Monadを中心としたライブラリ ○ 今回はTaskEitherの範囲での紹介 ○ Effect自体にはもっと豊富な機能があります ● ScalaのZIOをTypeScriptにポートしてきたライブラリ Effect-TS 38

Slide 39

Slide 39 text

© 2023 Leverages Co., Ltd. ● Effect の型を持つ ● Requirementsは今回触れませんが、副作用の分離にも 使えます Effect 39

Slide 40

Slide 40 text

© 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 TaskEitherがEffectに置き換わっただけ

Slide 41

Slide 41 text

© 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); }

Slide 42

Slide 42 text

© 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); }

Slide 43

Slide 43 text

© 2023 Leverages Co., Ltd. まとめ 43

Slide 44

Slide 44 text

© 2023 Leverages Co., Ltd. 1. ADTで型の表現力上げたい 2. 成功/失敗などまで型で表現してバグを減らしたい 3. Promiseも含めて一緒くたに扱いたい 4. そうだ、Effect-TS使おう 流れ 44

Slide 45

Slide 45 text

© 2023 Leverages Co., Ltd. ● 今回は、Step by Stepのためfp-tsを使ってモナド周りを説 明しました ● 実践ではEffect-TSだけで機能は実現できます Effect-TSだけ使えばOK 45

Slide 46

Slide 46 text

© 2023 Leverages Co., Ltd. ● バグが減る ○ ロジックまで型で表現できて、コンパイラがチェックして くれる ● 拡張性、メンテナンス性UP ○ 処理をより小さく分割し易い ○ データフローの定義になり、より宣言的なコードになる 効能 46

Slide 47

Slide 47 text

© 2023 Leverages Co., Ltd. 是非、Effect-TSを使って より良いサービスを作ってください 47

Slide 48

Slide 48 text

© 2023 Leverages Co., Ltd. We are hiring! 40を超えるサービスを、 すべてオールインハウスで開発! 現在レバレジーズでは一緒に働いてくれる仲間を募集しています。 もし、ご興味ある方は、以下のリンクからご応募ください。 https://recruit.leverages.jp/recruit/engineer/