제12장: 타입 순회하기(Traversing the Stone)
지금까지 컨테이너 서커스에서 여러분은 우리가 사나운 펑터를 길들여 임의의 연산을 수행하는 모습을 목격하셨습니다. 함수 적용으로 여러 효과를 동시에 다루며 결과를 수집하는 묘기를 경험하셨습니다. 결합을 통해 컨테이너들을 증발시키는 경이로운 현상을 목격하셨고, 부작용 부가 시스템에서는 이를 합성하여 단일 효과로 통합하는 모습을 보셨습니다. 최근엔 한 타입을 다른 타입으로 직접 변환하는 장면도 목도하셨습니다.
이제 새로운 트래버서블 트릭을 선보이겠습니다. 타입들이 공중그네 묘기사처럼 값을 보존하며 상호 교차하는 모습을 목격하게 될 겁니다. 회전 관람차처럼 효과들을 재배치하고, 곡예사 사지처럼 얽힌 컨테이너를 펼쳐보이겠습니다. 팬탈룬을 입고 피리슬을 준비하세요. 본격적으로 시작해 봅시다.
타입의 연쇄
기이한 세계로 들어가 봅시다:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
map(tldr, ['file1', 'file2']);
// [Task('hail the monarchy'), Task('smash the patriarchy')]여기서 여러 파일을 읽어 쓸모없는 태스크 배열을 얻었습니다. 이를 어떻게 분기처리할까요? 타입을 Task Error [String]으로 변환하면 모든 결과를 가진 단일 미래 값을 얻을 수 있어 비동기 처리에 적합합니다.
해결이 까다로운 사례를 살펴보죠:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe (IO Node))
const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $);IO들이 합치길 갈망하고 있습니다. Maybe가 무도회 감시인처럼 방해하는 이 상황에선 타입 위치를 재조정해 IO (Maybe Node)로 단순화하는 게 최선입니다.
타입 구조 개선
트래버서블 인터페이스는 sequence와 traverse 두 함수로 구성됩니다.
sequence를 이용한 타입 재배치:
sequence(List.of, Maybe.of(['the facts'])); // [Just('the facts')]
sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 }))
sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
sequence(Either.of, [Either.of('wing')]); // Right(['wing'])
sequence(Task.of, left('wing')); // Task(Left('wing'))중첩 타입이 여름밤의 가죽 바지처럼 안팎이 뒤집혔습니다. sequence는 t (f a)를 f (t a)로 변환하며 매핑을 꺼리는 타입 처리를 위한 생성자가 필요합니다.
// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, x) => x.sequence(of));트래버서블이 애플리커티브를 보유해야 하며, 생성자 제공으로 Left 같은 타입을 처리합니다. 타입 시스템이 있는 언어에선 외부 타입 추론이 가능합니다.
Either 구현 사례:
class Right extends Either {
// ...
sequence(of) {
return this.$value.map(Either.of);
}
}애플리커티브 펑터에 생성자 매핑으로 타입 변화가 가능합니다.
Left 처리 시 of 사용 사례:
class Left extends Either {
// ...
sequence(of) {
return of(this);
}
}펑터 범주의 정체성 증명에 Identity가 사용되며, Compose 타입도 유사하게 적용됩니다.
효과 다양성
[Maybe a]와 Maybe [a]는 각각 개별값 검증과 일괄 검증을 의미합니다. 타입 순서 변경으로 클라이언트/서버 검증을 표현할 수 있습니다.
// fromPredicate :: (a -> Bool) -> a -> Either e a
// partition :: (a -> Bool) -> [a] -> [Either e a]
const partition = f => map(fromPredicate(f));
// validate :: (a -> Bool) -> [a] -> Either e [a]
const validate = f => traverse(Either.of, fromPredicate(f));map과 traverse의 차이: partition은 필터링 결과 보존, validate는 전체 성공 시 반환하는 방식입니다.
List.traverse 구현 분석:
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}리스트 reduce 실행으로, 접기 함수 (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f)를 단계별로 풀어봅니다.
reduce시그니처는:: [a] -> (f -> a -> f) -> f -> f로, 점 표기법으로 리스트 제공됩니다.시드값
of(new List([]))는 최종 타입Either e [a]로 유지됩니다.fromPredicate(f) :: a -> Either e a적용 시 함수 타입 분석Either.map으로 클로저 활용, 함수 반환 과정 설명ap(f)애플리커티브 적용으로 결과 누적 처리
6줄 코드로 구현된 트래버서블의 강력한 추상화 능력, 애플리커티브 펑터의 일반화 코드 사례
타입의 춤사위
초기 예제 재점검 및 정리
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
traverse(Task.of, tldr, ['file1', 'file2']);
// Task(['hail the monarchy', 'smash the patriarchy']);Promise.all()과 유사한 트래버서블 범용 처리 방식 설명
최종 예제 정리:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe Node)
const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $);chain(traverse(IO.of, $))로 타입 변환 및 중첩 IO 처리
법칙과 질서
인터페이스 법칙이 코드 보장을 제공하며 아키텍처 설계의 핵심 역할 강조
법칙 없는 인터페이스는 간접 참조에 불과하며 캡슐화 효과를 지닙니다.
트래버서블 법칙 분석 시작
항등성
const identity1 = compose(sequence(Identity.of), map(Identity.of));
const identity2 = Identity.of;
// test it out with Right
identity1(Either.of('stuff'));
// Identity(Right('stuff'))
identity2(Either.of('stuff'));
// Identity(Right('stuff'))Identity로 타입 재배치 검증, 펑터 범주의 정체성 이론 설명
결합성
const comp1 = compose(sequence(Compose.of), map(Compose.of));
const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
// Test it out with some types we have lying around
comp1(Identity(Right([true])));
// Compose(Right([Identity(true)]))
comp2(Either.of, Array)(Identity(Right([true])));
// Compose(Right([Identity(true)]))펑터 합성의 법칙 준수, 퀵체크 도구 활용 사례
트래버설 융합으로 인한 성능 개선 이점
자연성
const natLaw1 = (of, nt) => compose(nt, sequence(of));
const natLaw2 = (of, nt) => compose(sequence(of), map(nt));
// test with a random natural transformation and our friendly Identity/Right functors.
// maybeToEither :: Maybe a -> Either () a
const maybeToEither = x => (x.$value ? new Right(x.$value) : new Left());
natLaw1(Maybe.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))
natLaw2(Either.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))자연 변환 순서의 동등성 법칙 설명
변환 과정 최적화의 성능적 이점
traverse(A.of, A.of) === A.of;법칙 준수로 인한 최적화 가능성
요약
정신적 인테리어 디자이너처럼 타입을 자재하는 트래버서블. 다음 장에선 대수학의 정수 모노이드를 다룹니다.
연습문제
다음 요소 고려:
// httpGet :: Route -> Task Error JSON
// routes :: Map Route Route
const routes = new Map({ '/': '/', '/about': '/about' });연습해 보세요!
트래버서블로 getJsons 타입 시그니처를 Map Route Route → Task Error (Map Route JSON)으로 변경
// getJsons :: Map Route Route -> Map Route (Task Error JSON)
const getJsons = map(httpGet);
검증 함수 정의:
// validate :: Player -> Either String Player
const validate = player => (player.name ? Either.of(player) : left('must have name'));연습해 보세요!
트래버서블과 validate로 startGame을 모든 플레이어 유효 시에만 시작하도록 업데이트
// startGame :: [Player] -> [Either Error String]
const startGame = compose(map(map(always('game started!'))), map(validate));
파일 시스템 헬퍼:
// readfile :: String -> String -> Task Error String
// readdir :: String -> Task Error [String]연습해 보세요!
트래버서블로 중첩 Task & Maybe 재배치 및 평탄화 // readFirst :: String -> Task Error (Maybe (Task Error String)) const readFirst = compose(map(map(readfile('utf-8'))), map(safeHead), readdir);