iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Software Development

Functional Programming For Everyone系列 第 11

Day 11 - Algebraic Data Types

yo, what's up?

Product Type

Product types 允許同時存在兩種以上的資料型態在內

舉例來說現在我們建立一個特別的型別叫 Clock,其可以放入兩個數值,HourPeriod

class Clock {
    constructor(hour, period) {
        if(!Array.from({length: 12}, (_, i) => i + 1).includes(hour) || !['AM', 'PM'].includes(period)){
            throw new Error('format error!')
        }
        this.hour = hour;
        this.period = period;
    }
}

大家可以思考一下,我們呼叫 Clock 可能的組合會有多少?

const One_AM = new Clock(1, "AM");
const Two_AM = new Clock(2, "AM");

// ... 以此類推

沒錯,是 24 種,因為 Hour 有 12 種, Period 則是 2 種,所以 Clock 會有 12 * 2 = 24 種不同的組合。

用在數學世界中,通常會將其表示

C([A, B]) = C(A) * C(B)

C(A): 為 type A 有多少種元素在內。例如 Hour 有 12 種。

而 Product type 運用的時機通常為其組成數值為相互獨立的。 像是 hour 改變時, period 並不會受到影響。

Sum Type

Sum types 則每次只能有一組固定的資料型態

用在數學世界中,通常會將其表示

C(A | B) = C(A) + C(B)

例如 TODO List 其狀態包含 CRUD, 並且會用 type 去標記現在的狀態 (ex CREATE, REMOVE)

const CREATE_TODO = 'CREATE_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

{
  type: CREATE_TODO,
  text
}

{
  type: REMOVE_TODO,
  id,
}

而每個 Action 都會有自己的 constructors

const create = (text) => ({
  type: CREATE_TODO,
  text
})

const remove = (id) => ({
  type: REMOVE_TODO,
  id
})

這就是 Sum Type 的概念, Action 裡面只會有一種動作,不會同時有多個,而每個動作都會用 tag 去標記,這也讓我們可以對其進行 recursive,舉 LinkedList 為例,

如果用 TS 的 interface 表達

type LinkedList<A> =
  | { readonly _tag: 'Nil' }
  | { readonly _tag: 'Cons'; readonly head: A; readonly tail: LinkedList<A> }

LinkedList<A> 就是 recursion.

Pattern matching

在一些 FP 語言中,有pattern matching 這個非常好用的功能。而 JavaScript 則有相關的 proposal 正在進行,但在原生沒有這個功能前,我們可以實作出一個類似的 pattern matching 的函式 match.

繼續沿用 LinkedList 作為範例,

const nil = { _tag: 'Nil' };

const cons = (head, tail) => ({
  _tag: 'Cons',
  head,
  tail,
});

const match = (onNil, onCons) => (fa) => {
  switch (fa._tag) {
    case 'Nil':
      return onNil();
    case 'Cons':
      return onCons(fa.head, fa.tail);
  }
};

// 此 LinkedList 是否為空
const isEmpty = match(
  () => true,
  () => false
);

// 新增 item 在 LinkedList 中
const addFirst = (num) =>
  match(
    () => cons(num, nil),
    (head, _tail) => cons(num, cons(head, _tail))
  );

// 取得該 LinkedList 第一個數值
const head = match(
  () => undefined,
  (head, _tail) => head
);

// 取得該 LinkedList 最後一個數值
const last = match(
  () => undefined,
  (head, tail) => (tail._tag === 'Nil' ? head : last(tail))
);

// 取得該 LinkedList 長度
const length = match(
  () => 0,
  (_, tail) => 1 + length(tail)
);


const myList = cons(1, cons(2, cons(3, nil)));

isEmpty(myList) // false

Sum Type in UI Display

如果兩值(狀態)相依時,以 react 為例,我們常常會寫出類似這樣的程式

import React, { useState, useEffect } from 'react';

const App = () => {
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState(null);
    const [data, setData] = useState(null);
    
    ...
    
    return <>{!loading && !error && data.map(/** rendering */)}</>
}

而這種兩值相依的情況就非常適合使用 Sum type,我們就來將上面改寫一下,

首先我們先定義其可能的狀態,並根據每個狀態給定 constructor.

const match = (onInit, onLoading, onError, onSuccess) => (fa) => {
  switch (fa._tag) {
    case 'INIT':
      return onInit();
    case 'LOADING':
      return onLoading();
    case 'ERROR':
      return onError(fa.error);
    case 'SUCCESS':
      return onSuccess(fa.data);
    default:
      break;
  }
};

const STATE = {
  INIT: { _tag: 'INIT' },
  LOADING: { _tag: 'LOADING' },
  ERROR: (error) => ({
    _tag: 'ERROR',
    error,
  }),
  SUCCESS: (data) => ({ _tag: 'SUCCESS', data }),
};

接下來進行 fetch 以及 UI render

import React, { useEffect, useState } from 'react';

export default function App() {
  const [result, setResult] = useState(STATE.INIT);

  useEffect(() => {
    const runEffect = () => {
      setResult(STATE.LOADING);

      fetch('https://jsonplaceholder.typicode.com/todos')
        .then((response) => response.json())
        .then((data) => setResult(STATE.SUCCESS(data)))
        .catch((error) => setResult(STATE.ERROR(error)))
    };

    runEffect();
  }, []);

  const renderer = match(
    () => <div>initial...</div>,
    () => <div>loading...</div>,
    (error) => <div>{JSON.stringify(error)}</div>,
    (xs) =>
      xs.map((x) => (
        <code key={x.id}>
          <pre>{JSON.stringify(x, null, 2)}</pre>
        </code>
      ))
  );

  return <>{renderer(result)}</>;
}

avaliable on stackblitz

小結

Product Type 適合用在兩值相互獨立的情況, Sum Type 則適合用在兩值相依的情況,而 ADT 的概念的應用在處理業務邏輯上。

NEXT: Semigroup

Reference

  1. ADT
  2. TS-ADT

上一篇
Day 10 - Algebraic structure
下一篇
Day 12 - Semigroup I
系列文
Functional Programming For Everyone30

1 則留言

1
Ken Chen
iT邦新手 5 級 ‧ 2021-10-01 22:24:09

喔喔喔,好酷喔 pattern matching

我要留言

立即登入留言