Maybe Monad로 null 체크 체이닝
type Maybe<T> = T | null
const Maybe = {
of: <T>(value: T): Maybe<T> => (value != null ? value : null),
map: <T, U>(m: Maybe<T>, fn: (v: T) => U): Maybe<U> =>
m != null ? Maybe.of(fn(m)) : null,
}
// 사용 예: DOM 요소 찾아서 스크롤
Maybe.of(scrollElement.current)
.map((root) => root.querySelector(`#${id}`))
.map((target) => target.getClientRects()[0])
.map((rect) => {
root.scrollLeft = rect.left
})
Generator로 early return 패턴도 가능
function* handleScroll(id) {
const root = scrollElement.current
if (!root) return
const target = root.querySelector(`#${id}`)
if (!target) return
yield target.getClientRects()[0]?.left ?? 0
}
모나드 입문
모나드 = 복잡한 처리를 숨기면서 연속된 연산을 가능하게 하는 패턴
- 타입 래퍼 - 값을 감싸는 구조 (예:
NumberWithLogs) - 래핑 함수 (unit/return) - 값을 모나드로 감쌈
- 바인딩 함수 (bind/flatMap) - 래핑된 값에 함수 적용
interface NumberWithLogs {
result: number
logs: string[]
}
function wrapWithLogs(n: number): NumberWithLogs {
return { result: n, logs: [] }
}
function runWithLogs(
input: NumberWithLogs,
transform: (n: number) => NumberWithLogs
): NumberWithLogs {
const next = transform(input.result)
return { result: next.result, logs: [...input.logs, ...next.logs] }
}
Option, Promise도 모나드. then이 바인딩 함수 역할.
단 Promise 는 근사다 — thenable 을 자동으로 펼쳐서 left identity(unit(x).bind(f) === f(x))가 깨진다.1
const v = { then: (r) => r(42) }
const f = (x) => Promise.resolve([x])
f(v) // → [v]
Promise.resolve(v).then(f) // → [42]
Footnotes
-
Promise.resolve() - JavaScript | MDN: “This function flattens nested layers of promise-like objects (e.g., a promise that fulfills to a promise that fulfills to something) into a single layer — a promise that fulfills to a non-thenable value.” ↩
TypeScript Maybe Monad 구현
type Maybe<T> = Just<T> | Nothing
class Just<T> {
constructor(public value: T) {}
bind<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
return fn(this.value)
}
}
class Nothing {
bind<U>(fn: (value: any) => Maybe<U>): Maybe<U> {
return this
}
}
const nothing = new Nothing()
function safeDivide(x: number, y: number): Maybe<number> {
return y === 0 ? nothing : new Just(x / y)
}
new Just(10).bind((x) => safeDivide(x, 2)) // Just { value: 5 }
new Just(10).bind((x) => safeDivide(x, 0)) // Nothing {}
콜백 → Promise → async/await
// 콜백
fetchData(() => {
processData(() => {
displayData()
})
})
// Promise 체인
fetchData().then(processData).then(displayData)
// async/await
async function main() {
const data = await fetchData()
const processed = await processData(data)
await displayData(processed)
}
Promise는 모나드처럼 동작 — then 은 콜백이 Promise 를 돌려주면 풀어서 잇는다(flatMap). 값을 돌려주면 감싼다(map). 한 메서드가 둘 다 해서 Promise<Promise<T>> 는 만들어지지 않는다.1
Footnotes
-
Promise.prototype.then() — MDN: “The value returned from then() is resolved in the same way as Promise.resolve(). This means thenable objects are supported, and if the return value is not a promise, it’s implicitly wrapped in a Promise and then resolved.” ↩