## 函數式程式設計在網頁前端開發的入門應用
### 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.
>
> — 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}]"}