함수형 프로그래밍과 Jotai
예상치 못한 유사점
getter 함수를 충분히 오래 살펴보면, 특정 자바스크립트 언어 기능과 놀라울 정도로 비슷하다는 것을 알 수 있습니다.
const nameAtom = atom('Visitor')const countAtom = atom(1)const greetingAtom = atom((get) => {const name = get(nameAtom)const count = get(countAtom)return (<div>Hello, {name}! You have visited this page {count} times.</div>)})
이 코드를 async
–await
와 비교해 보세요:
const namePromise = Promise.resolve('Visitor')const countPromise = Promise.resolve(1)const greetingPromise = (async function () {const name = await namePromiseconst count = await countPromisereturn (<div>Hello, {name}! You have visited this page {count} times.</div>)})()
이 유사성은 우연이 아닙니다. 아톰과 Promise는 모두 모나드(Monads)†라는 함수형 프로그래밍 개념입니다. greetingAtom
과 greetingPromise
에서 사용된 구문은 do-notation으로 알려져 있으며, 이는 더 단순한 모나드 인터페이스를 위한 문법적 설탕입니다.
모나드에 대해
모나드 인터페이스는 아톰과 Promise 인터페이스의 유연성을 담당합니다. 모나드 인터페이스 덕분에 greetingAtom
을 nameAtom
과 countAtom
으로 정의할 수 있었고, greetingPromise
를 namePromise
와 countPromise
로 정의할 수 있었습니다.
궁금하다면, Atom
이나 Promise
와 같은 구조체가 모나드가 되려면 다음 함수들을 구현할 수 있어야 합니다. 재미있는 연습으로, 배열에 대해 of
, map
, join
을 구현해 보는 것도 좋습니다.
type SomeMonad<T> = /* 예를 들어... */ Array<T>declare function of<T>(plainValue: T): SomeMonad<T>declare function map<T, V>(anInstance: SomeMonad<T>,transformContents: (contents: T) => V,): SomeMonad<V>declare function join<T>(nestedInstances: SomeMonad<SomeMonad<T>>): SomeMonad<T>
Promise와 아톰의 공통된 유산은 많은 패턴과 모범 사례가 둘 사이에서 재사용될 수 있음을 의미합니다. 그 중 하나를 살펴보겠습니다.
시퀀싱
콜백 지옥에 대해 이야기할 때, 우리는 보일러플레이트 코드, 들여쓰기, 그리고 놓치기 쉬운 실수에 대해 자주 언급합니다. 하지만 하나의 비동기 작업을 다른 하나의 비동기 작업에 연결하는 것이 콜백 문제의 끝은 아니었습니다. 만약 네트워크 호출을 네 번 하고 모두 기다려야 한다면 어떻게 될까요? 이런 코드 조각은 흔히 볼 수 있었습니다:
const nPending = 4const results: string[]function callback(err, data) {if (err) throw errresults.push(data)if (results.length === nPending) {// results로 무언가를 한다...}}
하지만 결과가 서로 다른 타입을 가지고 있고 순서가 중요하다면 어떻게 될까요? 그렇다면 훨씬 더 많은 고통스러운 작업을 해야 합니다! 이 로직은 각 사용처에서 중복될 것이고, 실수하기 쉬울 것입니다. ES6부터는 단순히 Promise.all
을 호출하면 됩니다:
declare function promiseAll<T>(promises: Array<Promise<T>>): Promise<Array<T>>
Promise.all
은 Array
와 Promise
를 "재배열"합니다. 이 개념, 즉 시퀀싱은 모든 모나드와 Traversable 쌍에 대해 구현될 수 있습니다. 많은 종류의 컬렉션이 Traversable에 속하며, 배열도 그 중 하나입니다. 예를 들어, 이는 아톰과 배열에 특화된 시퀀싱의 예입니다:
function sequenceAtomArray<T>(atoms: Array<Atom<T>>): Atom<Array<T>> {return atom((get) => atoms.map(get))}
정점
모나드는 수학자들에게는 60년 동안, 프로그래머들에게는 40년 동안 관심의 대상이었습니다. 모나드 패턴에 대한 많은 자료가 있습니다. 한번 살펴보세요! 여기 몇 가지 추천 자료를 소개합니다:
- Stepan Parunashvili의 Inventing Monads
- ThatsNoMoon의 How Monads Solve Problems
- 위키 페이지 list of monad tutorials
- Typeclassopedia (호기심이 많은 분들을 위해)
Promise를 사용하는 깔끔한 트릭을 배우는 것은 Promise.all
과 sequenceAtomArray
가 그랬던 것처럼 아톰에도 적용될 수 있습니다. 모나드는 마법이 아니며, 단지 유용한 도구일 뿐입니다. 알아두면 좋은 도구입니다.
참고
[†] ES6 Promise는 완전한 모나드가 아닙니다. 왜냐하면 다른 Promise를 중첩할 수 없기 때문입니다. 예를 들어, Promise<Promise<number>>
는 의미상 Promise<number>
와 동일합니다. 이 때문에 Promise는 .then
만 가지고 있고, .map
과 .flatMap
을 모두 가지고 있지 않습니다. ES6 Promise는 "모나드적"이라고 설명하는 것이 더 적절할 것입니다.
ES6 Promise와 달리, ES6 Array는 완전히 합법적인 모나드입니다.