Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで

Step by Stepで学ぶ、ADT(代数的データ型)、モナドからEffect-TSまで

TSKaigi2024で、テックリードの竹下が発表したセッションのスライドです。

※竹下は、一般社団法人TSKaigi Asscosiationの代表理事も兼任しております

Tech Leverages

May 13, 2024
Tweet

More Decks by Tech Leverages

Other Decks in Technology

Transcript

  1. © 2023 Leverages Co., Ltd. レバレジーズ株式会社 竹下 義晃 テクノロジー戦略室室長 一般社団法人TSKaigi

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

    map, flatMap, reduce • 関数型の概念も、根底にはなんらかの課題があり、それを 解決している • 私はJava,C# => Scalaで関数型に入門しました 4
  3. © 2023 Leverages Co., Ltd. 目次 1. このセッションでの説明範囲 2. 今日のサンプルコード

    3. 代数的データ型: リッチな型表現 4. Either: 成功/失敗を型で表現 5. TaskEither: 非同期処理も巻き込む 6. Effect-TS: すべてを一つに 5
  4. © 2023 Leverages Co., Ltd. 説明すること • ADT(代数的データ型), Either(Result型), TaskEither,

    Effect-tsを、型の表 現力に焦点を当ててStep by Stepで説明していきます 7 説明しないこと • 関数型言語や、モナドの理論的な話 • Effect-tsのCollection MonadやIO Monadなどの側面 • 各ライブラリ(fp-tsやEffect-TS)の詳しい使い方 • コードや設計自体の良し悪し
  5. © 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
  6. © 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 }
  7. © 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どちらが成功パ ターンになるか人依存 どんなエラーが投げられるかよくわからない
  8. © 2023 Leverages Co., Ltd. 問題点 12 • ソースコードを読まないと関数の挙動がわからな い

    ◦ 成功時はUserIDが帰って来そうだが、エラー 時は? • ifとかtryとか入り乱れる
  9. © 2023 Leverages Co., Ltd. 代数的データ型(ADT)とは 14 • 直積型と、直和型を組み合わせて表現される型 直積

    type User = {name : string, age: number} = nameとageの直積型 直和 type SNS = “Google” | “Facebook” | “X”
  10. © 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 {}
  11. © 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 {} どんなエラーが返ってくるかまで、表現できるよう になった
  12. © 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; }
  13. © 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; } 別々のフィールドをもたせられるようにもなってい る
  14. © 2023 Leverages Co., Ltd. 改善点と問題点 19 改善点 • 型定義を見るだけでなんとメソッドの挙動がわか

    る ◦ = 宣言的になった 問題点 • 依然としてif文多い ◦ 結果のチェックミスによるバグが発生する
  15. © 2023 Leverages Co., Ltd. Eitherとは 21 • Either<Left, Right>の型定義を持つMonad

    ◦ Right=正しいので、成功の型を表す • Result<Success>や、Result<Success, Failure>と同じ 使われ方 成功したか、失敗したかを型で表現できるようにしたもの
  16. © 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
  17. © 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; }
  18. © 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で判定すれば良い
  19. © 2023 Leverages Co., Ltd. 改善点と問題点 25 改善点 • 型で成功失敗までわかるようになった

    • 条件判定も画一化した 問題点 • 依然としてif文多い • むしろ冗長になっている
  20. © 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)) ); }
  21. © 2023 Leverages Co., Ltd. pipeの導入(型の確認) 27 type CreateUserValidationError =

    { type: "ValidationError"; field: string; message: string } type ValidateInputError = | { type: "InvalidEmail"; message: string } | { type: "InvalidPassword"; message: string }
  22. © 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)) ); } エラー型を戻り値の型に変換する処理として分離
  23. © 2023 Leverages Co., Ltd. 改善点と問題点 29 改善点 • if文がなくなった

    ◦ データフローとして表現できるように • 処理を分割して、組み合わせやすくなった 問題点 • pipeの書き方/読み方に慣れる必要ある ◦ 型パズルの一端 • あれ?Promiseは? ◦ saveとか普通Promiseだよね?
  24. © 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)) ) }
  25. © 2023 Leverages Co., Ltd. TaskとTaskEither 32 • Task<T>は、”処理”を型で表現したMonad ◦

    実態は() => Promise<T> ◦ Taskは作るだけではまだ実行されていない ◦ 作ってrunしたタイミングで初めて処理が実行 される • TaskEither<L,R>はTaskとEitherをMonad Transformerで合成した、新しいMonad
  26. © 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に置き換わっただけ
  27. © 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();
  28. © 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への変換が発生
  29. © 2023 Leverages Co., Ltd. 改善点と問題点 36 改善点 • PromiseもMonadに巻き込めた

    問題点 • 型パズル感UP ◦ TaskをTaskEitherへ変換が必要
  30. © 2023 Leverages Co., Ltd. • Effect Monadを中心としたライブラリ ◦ 今回はTaskEitherの範囲での紹介

    ◦ Effect自体にはもっと豊富な機能があります • ScalaのZIOをTypeScriptにポートしてきたライブラリ Effect-TS 38
  31. © 2023 Leverages Co., Ltd. • Effect<Success, Error = unknown,

    Requirements = unknown> の型を持つ • Requirementsは今回触れませんが、副作用の分離にも 使えます Effect 39
  32. © 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に置き換わっただけ
  33. © 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); }
  34. © 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); }
  35. © 2023 Leverages Co., Ltd. 1. ADTで型の表現力上げたい 2. 成功/失敗などまで型で表現してバグを減らしたい 3.

    Promiseも含めて一緒くたに扱いたい 4. そうだ、Effect-TS使おう 流れ 44
  36. © 2023 Leverages Co., Ltd. • 今回は、Step by Stepのためfp-tsを使ってモナド周りを説 明しました

    • 実践ではEffect-TSだけで機能は実現できます Effect-TSだけ使えばOK 45
  37. © 2023 Leverages Co., Ltd. • バグが減る ◦ ロジックまで型で表現できて、コンパイラがチェックして くれる

    • 拡張性、メンテナンス性UP ◦ 処理をより小さく分割し易い ◦ データフローの定義になり、より宣言的なコードになる 効能 46
  38. © 2023 Leverages Co., Ltd. We are hiring! 40を超えるサービスを、 すべてオールインハウスで開発!

    現在レバレジーズでは一緒に働いてくれる仲間を募集しています。 もし、ご興味ある方は、以下のリンクからご応募ください。 https://recruit.leverages.jp/recruit/engineer/