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

<<:  第 10 天 阶段达成继续奋斗( leetcode 003 )

>>:  【Day26】 音乐如何引起人的情绪

[重构倒数第13天] - Vue3定义自己的模板语法

前言 该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系...

# Day18--如果我早一点追求MVC小姊姊,我就不会乱写了

据说设计模式有很多种⋯⋯而MVC是超级常用的一种,在还没有物件导向的概念、或者是值型别、参考型别的概...

第10-2章:监控与管理作业系统上之程序(二)

前言 在上一章节中,讲述了Linux process之基本原理与机制,以及控制jobs工作的方法,并...

[Day29] 第二十九课 Azure灾害复原(DRaaS)-2[进阶]

我们来接续昨日Azure Site Recovery(ASR)的进度之前, 我想补充一下地端及云端容...

IT 铁人赛 k8s 入门30天 -- day2 k8s 元件介绍

前言 为了能够更全面的去理解k8s的原理 今天主要从k8s 几个基础的元件开始介绍 Node &am...