iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0

https://ithelp.ithome.com.tw/upload/images/20250831/20118113BLkCodRJyL.png
今天要從頭開始打造一個Monad類別的Response模組,將各類別的定義複習一次,以後再看到這些FP的類別名詞時就不會再陌生。

定義

依照昨天的內容定義常數URI和型別URI皆為'Response',並定義 Response 型別容器,容器中由型別參數決定型別的屬性為body。

// 類別介面模組匯入
import { Kind, URIS } from 'fp-ts/HKT';
import { Pointed1 } from 'fp-ts/Pointed';
import { Functor1 } from 'fp-ts/Functor';
import { Apply1 } from 'fp-ts/Apply';
import { Applicative1 } from 'fp-ts/Applicative';
import { Chain1 } from 'fp-ts/Chain';
import { Monad1 } from 'fp-ts/Monad';
import { Foldable1 } from 'fp-ts/Foldable';
import { Traversable1 } from 'fp-ts/Traversable';
import { Monoid } from 'fp-ts/Monoid';

// 1. 定義 URI
export const URI = 'Response';
export type URI = typeof URI;

// 2. 擴展 fp-ts 的 URI 映射
declare module 'fp-ts/HKT' {
  interface URItoKind<A> {
    readonly [URI]: Response<A>;
  }
}

// 3. 定義 Response 型別容器
export interface Response<A> {
  readonly _tag: URI;
  readonly url: string;
  readonly status: number;
  readonly headers: Record<string, string>; // 物件型別
  readonly body: A;
}

建構函數

// 4. 通用構造函數
export const make = <A>(
  url: string,
  status: number,
  headers: Record<string, string>,
  body: A
): Response<A> => ({
  _tag: URI,
  url,
  status,
  headers,
  body,
});

Monad類別函數與類別實例

接下來建構Monad類別所需的函數(of, map, ap, flatten, flatMap)

export const of = <A>(body: A): Response<A> => make('', 200, {}, body);

export const map =
  <A, B>(f: (a: A) => B) =>
  (fa: Response<A>): Response<B> =>
    make(fa.url, fa.status, fa.headers, f(fa.body));

export const ap =
  <A>(fa: Response<A>) =>
  <B>(fab: Response<(a: A) => B>): Response<B> =>
    make(fa.url, fa.status, fa.headers, fab.body(fa.body));

export const flatten = <A>(ffa: Response<Response<A>>): Response<A> => ({
  ...ffa, // 優先選擇外層url, status
  headers: { ...ffa.headers, ...ffa.body.headers }, // 合併內外層headers
  body: ffa.body.body,
});

const flatMap =
  <A, B>(f: (a: A) => Response<B>) =>
  (fa: Response<A>): Response<B> =>
    flatten(map(f)(fa));

const chain = flatMap;

再來建立Pointed、Functor、Apply、Applicative和Monad的類別實例。

// Pointed: of
export const Pointed: Pointed1<URI> = {
  URI,
  of,
};

// Functor: map
export const Functor: Functor1<URI> = {
  URI,
  map: (fa, f) => map(f)(fa),
};

const double = (x: number) => 2 * x;
const tripleR = (x: number) => RE.of(3 * x);
const add2 = (x: number) => (y: number) => x + y;

console.log(pipe(RE.of(5), RE.map(double), RE.flatMap(tripleR))); // RE.of(30)
console.log(pipe(RE.of(add2), RE.ap(RE.of(3)), RE.ap(RE.of(4)))); // RE.of(7)

// Apply: map, ap
export const Apply: Apply1<URI> = {
  ...Functor,
  ap: (fab, fa) => ap(fa)(fab),
};

// Applicative: of, map, ap
export const Applicative: Applicative1<URI> = {
  ...Apply,
  of,
};

// Chain: map, ap, chain(flatMap)
export const Chain: Chain1<URI> = {
  ...Apply,
  chain: (fa, f) => chain(f)(fa),
};

// Monad: of, map, ap, chain
export const Monad: Monad1<URI> = {
  ...Applicative,
  chain: (fa, f) => chain(f)(fa),
};

Functor的map函數必須遵守下列兩個規則:

  • Identity: F.map(fa, a => a) <-> fa
  • Composition: F.map(fa, a => bc(ab(a))) <-> F.map(F.map(fa, ab), bc) 先合成再map的結果和先map再合成的結果一樣

Apply中的ap也必須滿足下列規則:

  • Associative composition: F.ap(F.ap(F.map(fbc, bc => ab => a => bc(ab(a))), fab), fa) <-> F.ap(fbc, F.ap(fab, fa)) 結合律

Applicative除了滿足Apply的規則之外,還有滿足下列三個規則:

  • Identity: A.ap(A.of(a => a), fa) <-> fa
  • Homomorphism: A.ap(A.of(ab), A.of(a)) <-> A.of(ab(a))
  • Interchange: A.ap(fab, A.of(a)) <-> A.ap(A.of(ab => ab(a)), fab)

Chain除了滿足Apply的規則之外,還有滿足下面的規則:

  • Associativity: F.chain(F.chain(fa, afb), bfc) <-> F.chain(fa, a => F.chain(afb(a), bfc))

Monad除了滿足Applicative和Chain的規則之外,還有滿足下列二個規則:

  • Left identity: M.chain(M.of(a), f) <-> f(a)
  • Right identity: M.chain(fa, M.of) <-> fa

Traversable類別函數與介面實例

最後再完成和Traversable有關的函數和介面。

export const reduce =
  <A, B>(b: B, f: (b: B, a: A) => B) =>
  (fa: Response<A>): B =>
    f(b, fa.body);

export const foldMap =
  <B>(m: Monoid<B>) =>
  <A>(f: (a: A) => B) =>
  (fa: Response<A>): B =>
    f(fa.body);

export const reduceRight =
  <A, B>(b: B, f: (a: A, b: B) => B) =>
  (fa: Response<A>): B =>
    f(fa.body, b);
// Foldable: reduce, reduceRight, foldMap
export const Foldable: Foldable1<URI> = {
  URI,
  reduce: (fa, b, f) => reduce(b, f)(fa),
  foldMap: (M) => (fa, f) => foldMap(M)(f)(fa),
  reduceRight: (fa, b, f) => reduceRight(b, f)(fa),
};

export const traverse =
  <F extends URIS>(Ap: Applicative1<F>) =>
  <A, B>(f: (a: A) => Kind<F, B>) =>
  (ta: Response<A>): Kind<F, Response<B>> =>
    Ap.map(f(ta.body), (b: B) => make(ta.url, ta.status, ta.headers, b));

export const sequence =
  <F extends URIS>(Ap: Applicative1<F>) =>
  <A>(ta: Response<Kind<F, A>>): Kind<F, Response<A>> =>
    Ap.map(ta.body, (a: A) => make(ta.url, ta.status, ta.headers, a));

// Traversable: map, reduce, reduceRight, foldMap, traverse, sequence
export const Traversable: Traversable1<URI> = {
  ...Functor,
  ...Foldable,
  traverse:
    <F extends URIS>(Ap: Applicative1<F>) =>
    <A, B>(ta: Response<A>, f: (a: A) => Kind<F, B>) =>
      traverse(Ap)(f)(ta),
  sequence,
};

sequence和traverse函數從下列兩個式子中可以視為功能相容的函數:

  • traverse(A)(xs, f) <-> sequence(A)(A.map(xs, f))
  • sequence(A)(xs) <-> traverse(A)(xs, identity)

今日小結

今天用極其簡單的例子打造一個Response模組,主要的目的是複習所有FP類別介面所需要的函數,大家可以將今天的內容當作字典查詢,不需要反覆翻閱各類別介面的規定。今天內容就分享到這邊,明天再見。


上一篇
Day 24. 高階類別 - Higher Kinded Types(HKT)
下一篇
Day 26. Comonad - Tree
系列文
數學老師學函數式程式設計 - 以fp-ts啟航26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言