## 函數式程式設計在網頁前端開發的入門應用 ### caasih --- ## Sum Type 和 Product Type ---- ## ML 系 ```haskell= data List a = Nil | Cons a (List a) ``` ---- ## TypeScript ```typescript= interface Nil { type: 'nil' } interface Cons<T> { type: 'cons' head: T tail: List<T> } type List<T> = Nil | Cons<T> ``` --- ## 靠抽象層切割問題 ---- > We're going to solve that problem by using this incredbly powerful design strategy that you already seen us use over and over. > > And that's the strategy of wishful thinking. > > &mdash; Harold Abelson ---- ```typescript= export type Rat = [number, number] export declare makeRat = (numer, denom) => Rat export declare numer = (rat: Rat) => number export declare denom = (rat: Rat) => number ``` ---- ```typescript= export interface ToDoItem { id: string title: string done: boolean } ``` ---- ```typescript= export const create = (id: string, title: string, done: boolean): ToDoItem => ({ id, title, done }) export const id = (item: ToDoItem): string => item.id export const title = (item: ToDoItem): string => item.title export const done = (item: ToDoItem): boolean => item.done export const update = (done: boolean) => (item: ToDoItem) => ({ id: item.id, title: item.title, done, }) ``` ---- ```typescript= export type ToDoItem = [string, string, boolean] export const create = (id: string, title: string, done: boolean): ToDoItem => [id, title, done] export const id = ([id]: ToDoItem): string => id export const title = ([, title]: ToDoItem): string => title export const done = ([,, done]: ToDoItem): boolean => done export const update = (done: boolean) => ([id, title]: ToDoItem) => [id, title, done] ``` ---- ```typescript= import * as TDI from './ToDoItem' export interface ToDoList { key: number todos: TDI.ToDoItem[] } ``` ---- ```typescript= export const create = (): ToDoList => ({ key: 0, todos: [] }) export const append = (title: string) => (list: ToDoList): ToDoList => { const { key: prev, todos } = list const key = prev + 1 const item = TDI.create(`${key}`, title, false) return { key, todos: [...todos, item] } } ``` ---- ```typescript= export const updateById = (id: string, done: boolean) => (list: ToDoList): ToDoList => { const { key, todos } = list const i = todos.findIndex((item) => item.id === id) if (i === -1) return list return { key, todos: [ ...todos.slice(0, i), TDI.update(done)(todos[i]), ...todos.slice(i + 1) ] } } ``` ---- ```typescript= export const removeById = (id: string) => (list: ToDoList): ToDoList => { const { key, todos } = list const i = todos.findIndex((item) => item.id === id) if (i === -1) return list return { key, todos: [ ...todos.slice(0, i), ...todos.slice(i + 1) ] } } ``` --- ## 在不同的語境中工作 ---- ## OOP ```typescript= import * as TDL from './ToDoList' export default class ToDoList { private _toDoList: TDL.ToDoList constructor() { this._toDoList = TDL.create() } append(title: string) { this._toDoList = TDL.append(title)(this._toDoList) } updateById(id: string, done: boolean) { this._toDoList = TDL.updateById(id, done)(this._toDoList) } } ``` ---- ## Promise ```javascript= Promise.resolve(toDoList) .then(TDL.append('foobar')) .then(TDL.updateById('0', true)) ``` ---- ## React Hooks ```typescript= const Comp = () => { const [toDoList, setToDoList] = useState(TDL.create()) const handleToDoUpdate = (id: string, done: boolean) => { setToDoList(TDL.updateById(id, done)) } } ``` --- ## `|>` operator ---- ## F# style ```javascript= toDoList |> TDL.append('foobar') |> TDL.updateById('0', true) ``` ---- ## Hack style ```javascript= toDoList |> TDL.append('foobar')(%) |> TDL.updateById('0', true)(%) ``` ---- > **Both** Hack pipes and F# pipes respectively impose a small **syntax tax** on different expressions: > **Hack pipes** slightly tax only **unary function calls**, and > **F# pipes** slightly tax **all expressions except** unary function calls. ---- > But with F# pipes, `value |> someFunction + 1` is **still valid syntax** – it’ll just **fail late** at **runtime**, because `someFunction + 1` isn’t callable ---- 讀一下 [tc39/proposal-pipeline-operator](https://github.com/tc39/proposal-pipeline-operator/) ,看看你喜歡哪一種? --- ## Redux ---- ```typescript= interface Action<T> { type: 'redux/someAction' payload: T } ``` ---- ```typescript= interface RequestAsyncAction<C> { type: 'async/request' context: C } interface SuccessAsyncAction<C, T> { type: 'async/success' context: C payload: T } interface FailureAsyncAction<C> { type: 'async/failure' context: C error: Error } export type AsyncAction<C, T> = | RequestAsyncAction<C> | SuccessAsyncAction<C, T> | FailureAsyncAction<C> ``` ---- ```typescript= export const request = <C>(context: C): RequestAsyncAction<C> => ({ type: 'async/request', context, }) export const success = <C, T>(context: C, payload: T): SuccessAsyncAction<C, T> => ({ type: 'async/success', context, payload }) export const failure = <C>(context: C, error: Error): FailureAsyncAction<C> => ({ type: 'async/failure', context, error }) ``` ---- ```typescript= export const append = (title: string) => async (dispatch: AppendDispatch) => { dispatch(AsyncAction.request(title)) try { const item = await ToDoService.create(title) dispatch(AsyncAction.success(title, ToDoListAction.append(item))) } catch (err: any) { dispatch(AsyncAction.failure(title, err)) } } ``` ---- ```typescript= const reducer = (state = initialState, action: Action): State => { switch (action.type) { case 'async/request': { return { ...state, isLoading: true, } } case 'async/success': { const act = action.payload as ToDoListAction.Action return { isLoading: false, list: toDoListReducer(state.list, act) } } case 'async/failure': { return { ...state, isLoading: false, } } default: return state } } ``` ---- ```typescript= const reducer = (state = initialState, action: Action): TDL.ToDoList => { switch (action.type) { case 'todos/append': { return TDL.append(action.payload)(state) } case 'todos/updateById': { const [id, item] = action.payload return TDL.updateById(id, item)(state) } case 'todos/removeById': { return TDL.removeById(action.payload)(state) } default: return state } } ``` ---- ## Redux Toolkit - `"some/action/pending"` - `"some/action/fulfilled"` - `"some/action/rejected"` --- ## 網頁前端裡的 CPS 變換 ---- ```typescript= function add(a: number, b: number): number { return a + b } console.log(add(1, 2)) ``` ```typescript= function add(a: number, b: number, cb: (x: number) => void): void { cb(a + b) } add(1, 2, (c) => console.log(c)) ``` ---- ```typescript= function add(a: number, b: number): Promise<number> { return Promise.resolve(a + b) } add(1, 2).then(console.log) ``` ```typescript= const c = await add(1, 2) console.log(c) ``` ---- ```javascript= const Comp = () => { const p = useMemo(() => add(1, 2), []) const c = usePromise(p) return ( <span>1 + 2 = {c}</span> ) } ``` ---- ```javascript= import { useState, useMemo, useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; export default function useObservable(input$, initialState) { const [state, setState] = useState(initialState); const state$ = useMemo(() => new BehaviorSubject(initialState), []); useEffect(() => { const sub = state$.subscribe(x => setState(x)); return sub.unsubscribe.bind(sub); }, [state$]); useEffect(() => { const sub = input$.subscribe(state$.next.bind(state$)); return sub.unsubscribe.bind(sub); }, [input$]); return state; } ``` ---- ## Vue3 ```javascript= import { createComponent } from '@vue/composition-api' import { usePromise } from 'vue-compose-promise' export default createComponent({ setup() { const promised = usePromise({ pendingDelay: 200, promise: fetchUsers() }) return { usersPromise: promised.state, fetchUsers() { promised.state.promise = fetchUsers() }, } }, }) ``` ---- ```javascript= import { useState, useEffect } from 'react'; export default function useArray(xs) { const [s, set] = useState(xs[0]); useEffect(() => { for (let v of xs) { setImmediate(set, v); } }, [xs]); return s; } ``` --- ## 網頁前端的 Lifting ---- ```typescript= export type LazyPromise<T> = () => Promise<T> export const of = <T>(x: T) => () => Promise.resolve(x) export const then = <T, U>(f: (x: T) => LazyPromise<U>) => (lp: LazyPromise<T>): LazyPromise<U> => () => lp().then((x: T) => f(x)()) ``` ---- ```typescript= import * as LP from './LazyPromise' const one = LP.of(1) const two = LP.of(2) describe('LazyPromise', () => { it('should chain lazy promises', async () => { const run = one |> LP.then((x) => two |> LP.then((y) => LP.of(x + y) )) expect(await run()).toBe(3) }) }) ``` --- ## Code Examples - https://github.com/caasi/coscup-2022-fp-frontend --- ## Further Readings ---- ## Books & Papers - [SICP](https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book.html) - [Domain Modeling Made Functional](https://pragprog.com/titles/swdddf/domain-modeling-made-functional/) - [Comprehending Monads](https://ncatlab.org/nlab/files/WadlerMonads.pdf) - [How to make ad-hoc polymorphism less ad hoc](https://dl.acm.org/doi/10.1145/75277.75283) ---- ## Libraries - [fantasy-land](https://github.com/fantasyland/fantasy-land) - [fp-ts](https://gcanti.github.io/fp-ts/)
{"metaMigratedAt":"2023-06-17T05:54:23.709Z","metaMigratedFrom":"YAML","title":"函數式程式設計在網頁前端開發的入門應用","breaks":true,"contributors":"[{\"id\":\"7827bd06-2a37-46be-9f5c-7d5256d3702e\",\"add\":9903,\"del\":52}]","description":"We're going to solve that problem by using this incredbly powerful design strategy that you already seen us use over and over."}
    816 views