iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0

https://ithelp.ithome.com.tw/upload/images/20250831/20118113BLkCodRJyL.png

綜合實例

模組匯入

因為匯入的模組和函數甚多,為了避免一個一個函數匯入過於冗長且易於撞名需另外改名,所以採取整個模組匯入的模式。

import * as O from 'fp-ts/Option';
import * as IO from 'fp-ts/IO';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import * as R from 'fp-ts/Reader';
import { pipe, flow } from 'fp-ts/function';
import { log, error } from 'fp-ts/Console';

Helper function

為了模擬非同步執行,置入之前寫過的delay函數。

type Delay = (ms: number) => Task<void>;
export const delay: Delay = (ms) => async () =>
  new Promise((resolve) => setTimeout(resolve, ms));

接著定義User、Data和Deps三個型別,Deps型別是作為Reader的依賴注入型別,我們用interface來定義,以作為區別。為了增加練習時,型別的豐富性,Deps型別模擬了有同步、非同步的工作,錯誤處理上也有Option的型別。db和auth屬性模擬非同步函數操作;templateRenderer和logger屬性則模擬同步操作;config屬性則是靜態資料型別。

type User = {
  id: number;
  username: string;
  email: string;
}

type Data = {
  appName: string;
  user: User;
  isPremium: boolean;
}

interface Deps {
  // 資料庫查詢 - 使用安全型別 T.Task<O.Option<User>> 建構輸出,可以處理無法查詢的處理
  db: {
    getUserById: (id: number) => T.Task<O.Option<User>>;
  };

  // 權限資格服務 - 使用 T.Task型別處理非同步操作
  auth: {
    userHasPermission: (userId: number, permission: string) => T.Task<boolean>;
  };

  // 模板渲染器 - 使用 IO.IO 同步操作型別建構函數輸出
  templateRenderer: {
    render: (templateName: string, data: Data) => IO.IO<string>;
  };

  // 日誌參生器 - 也是一個同步操作的IO.IO型別
  logger: {
    info: (message: string) => IO.IO<void>;
    error: (message: string) => IO.IO<void>;
  };

  // 靜態應用配置 - 純數據,不需要嵌套任何型別建構子
  config: {
    appName: string;
    features: {
      isPremiumEnabled: boolean;
    };
  };
}

接下來建立getUser函數,它的型別簽名是

type GetUser = (userId: number) => R.Reader<Deps, TE.TaskEither<Error, User>>;
const getUser: GetUser = (userId: number) =>
({ db: { getUserById }, logger: { info } }) =>
    pipe(
      getUserById(userId), // T.Task<O.Option<User>>
      T.flatMap(TE.fromOption(() => Error(`User ${userId} not found`))), // TE.TaskEither<Error,O.Option<User>>
      TE.tapIO(() => info(`Fetching user ${userId}`)) // TE.TaskEither<E, User>
    );

程式解說:

  1. 將Deps型別解構需要的部分{ db: { getUserById }, logger: { info } }。
  2. Option沒有錯誤訊息,使用TE.fromOption補上錯誤訊息,並將輸出型別轉為TaskEither。
  3. tapIO插入IO,列印訊息,不影響原的輸出。

程式的另一種寫法為:

const getUser: GetUser = (userId: number) =>
  pipe(
    R.ask<Deps>(), // R.Reader<Deps, Deps>
    R.map(({ db: { getUserById }, logger: { info, error } }) =>
      pipe(
        getUserById(userId), // T.Task<O.Option<User>>
        T.flatMap(TE.fromOption(() => Error(`User ${userId} not found`))), // TE.TaskEither<Error, O.Option<User>>
        TE.tapIO(() => info(`Fetching user ${userId}...`)) // TE.TaskEither<E, User>
      ) // R.map(Deps => TE.TaskEither<E, User>) 等同 R.Reader<Deps, Deps> => R.Reader<Deps, TE.TaskEither<E, User>>
    )
  );

接下來的權限檢查函數checkPermission和模板渲染函數renderProfileTemplate,它們的邏輯也是類似,型別簽名和實作分別如下:

type CheckPermission = (
  userId: number,
  permission: string
) => R.Reader<Deps, TE.TaskEither<Error, boolean>>;
const checkPermission: CheckPermission =
  (userId, permission) =>
  ({ auth: { userHasPermission }, logger: { info } }) =>
    pipe(
      userHasPermission(userId, permission), //
      TE.fromTask,
      TE.mapError((error) => new Error(`Permission check failed: ${error}`)),
      TE.tapIO(() =>
        info(`Checking permission '${permission}' for user ${userId}`)
      )
    );

type RenderProfileTemplate = (user: User) => R.Reader<Deps, IO.IO<string>>;
const renderProfileTemplate: RenderProfileTemplate =
  (user: User) =>
  ({
    templateRenderer: { render },
    config: {
      appName,
      features: { isPremiumEnabled },
    },
    logger: { info },
  }) =>
    pipe(
      render('user-profile', {
        user,
        appName: appName,
        isPremium: isPremiumEnabled,
      }),
      IO.tap((t) =>
        info(`Rendering profile for user ${user.username}, template: ${t}`)
      )
    );

和getUser不同的是要從Task型別轉換到TaskEither型別,所以使用TE.mapError函數,並給予錯誤訊息。

至於主程式getUserProfile的實作則使用Do notation來綁定user和canView兩個值,完整的程式碼和型別簽名如下:

type GetUserProfile = (
  userId: number
) => R.Reader<Deps, TE.TaskEither<Error, User>>;
const getUserProfile: GetUserProfile = (userId) => (deps: Deps) =>
  pipe(
    TE.Do, // T.TaskEither<{}>
    TE.bind('user', () => getUser(userId)(deps)), // T.TaskEither<{user: User}>
    TE.bind('canView', () => checkPermission(userId, 'VIEW_PROFILE')(deps)), // T.TaskEither<{user: User, canView: boolean}>
    TE.flatMap(({ user, canView }) =>
      canView
        ? TE.right(user)
        : TE.left(new Error('User does not have permission to view profile'))
    ),
    TE.tapIO((user) => renderProfileTemplate(user)(deps))
  );

最後實作介面Deps的實例realDependencies:

const realDependencies: Deps = {
  db: {
    getUserById: (id) => async () => {
      console.log(`[DB] Querying user ${id}`);
      await delay(1000)();
      if (id === 1)
        return O.some({
          id: 1,
          username: 'johndoe',
          email: 'john@example.com',
        });
      return O.none;
    },
  },
  auth: {
    userHasPermission: (userId, permission) => async () => {
      console.log(`[AUTH] Checking ${permission} for user ${userId}`);
      await delay(500)();
      return userId === 1;
    },
  },
  templateRenderer: {
    render: (templateName, data) => () => {
      console.log(`[TEMPLATE] Rendering ${templateName} with data:`, data);
      return `
        <html>
          <head><title>${data.appName} - Profile</title></head>
          <body>
            <h1>Welcome, ${data.user.username}!</h1>
            <p>Email: ${data.user.email}</p>
            ${data.isPremium ? '<p>Premium Member!</p>' : ''}
          </body>
        </html>
      `;
    },
  },
  logger: {
    info: (msg) => log(`[INFO] ${new Date().toISOString()}: ${msg}`),
    error: (msg) => error(`[ERROR] ${new Date().toISOString()}: ${msg}`),
  },
  config: {
    appName: 'My Awesome App',
    features: {
      isPremiumEnabled: true,
    },
  },
};

以上是純函數的部分,接下來我們要讓主程式注入Deps的實例realDependencies並執行它。

const getUserProgram = (n: number) =>
  pipe(
    getUserProfile(n),
    R.map(
      TE.match(
        (e) => console.log(`Error:${e.message}`),
        (user) => console.log('Success!')
      )
    )
  );
getUserProgram(1)(realDependencies)();

今日小結

使用函數式設計時最重要的事就注意型別的推論,為了函數能順利合成接管,型別之間適時轉換也很重要。今天的範例包含了同步、非同步工作和IO工作,錯誤處理則有Option和Either之間的轉換,也練習了Reader的使用。今日的內容分享到此,明天再見。


上一篇
Day 19. 函數型別容器 - Reader & State
系列文
數學老師學函數式程式設計 - 以fp-ts啟航20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言