1. interface
- Traverse 는 higher-order fuction 입니다.
- 일반 value 를 monodic type 으로 lift 를 하면서 multi instance 를 single instance 로 바꿔줍니다.
- 말이 어렵긴 한데 실제로 예시를 보면 간단합니다.
1.1. signature
<F,A,B> (f: Applicative<F>) => (fa: (a:A => HKT<F, B>)) => (fb: A[]) => HKT<F, B[]>
- Applicative 를 받습니다. value 를 lift 하기 위해서입니다. 이는 sequecne 에서 사용하는 형태와 동일합니다. 이후 function 을 return 합니다.
- function 을 sigature 로 받습니다. 저 모양 자체는 map 사용처랑 흡사합니다(단지 해당 function 의 return 이 HKT type 인것만 제외하면)
- values 를 받고 이를 monodic< B[]> 으로 전환합니다. 이 부분도 sequence 랑 비슷합니다.
2. 어디에 쓰이는가?
- 위에서 이미 언급 되었지만 sequence 와 상당히 비슷합니다.
- sequence 역시 간단히 요약하면 A[] => HKT<F, A[]> 로 변경하는 것 이기때문 입니다.
- 다만 sequence 와의 차이점이 있다고 하면 traverse 는 lift 를 할 수 있는 function 을 받아서 사용 됩니다.
- 즉 sequence 는 이미 lift 된 value 에 대한 작업을 하는 반면에 traverse 는 아예 raw 한 value 들로부터 작업을 할 수 있습니다.
2.1. 사용하는 이유
- raw value(=not monodic type value) 를 lift 하면서 single instance 로 만들기 위해 사용합니다.
- 현실적인 예시가 아직은 떠오르지 않아서 차후 업데이트 하겠습니다.
2.2. 예시
2.2.1. 특정 숫자들을 Either 로 변경
import {array} from 'fp-ts'
import * as E from 'fp-ts/Either'
const numbers = [1,2,3]
const convertToEither = (val:number)=>E.right(val)
const res = array.traverse(E.Applicative)(convertToEither)(numbers)
- A[] => HKT<F, T[]> 형태로 변경되었습니다.
- 또한 traverse 가 사용된 모습을 보면 위에서 언급한 interface 를 그대로 읽을 수 있습니다.
- 만약 위 code 를 sequence 로 해야한다고 하면 이렇게 되어야합니다.
import { sequence } from 'fp-ts/lib/Array';
import * as E from 'fp-ts/Either'
const numbers = [1,2,3]
const eitherNumbers = numbers.map((val:number)=>E.right(val))
const res = sequence(E.Applicative)(eitherNumbers)
- 뭔가 엄청난 차이가 있어야 할 것 같지만 사실 그렇지 않습니다.
- 결국 raw value 를 쓰는가(=traverse) 아니면 monodic type value를 쓰는가의 사용하는 value 의 차이만이 존재할 뿐 입니다.
- 다만 error(left) 를 다루는 점에선 차이가 존재해서 이 부분은 아래에서 추가로 다루도록 하겠습니다.
CF) traverse & sequence 를 사용하지 않는 경우는 아래와 같습니다. 왜 traverse 와 sequence 를 써야하는지 단번에 알 수 있습니다 ㅎㅎ
import {pipe} from 'fp-ts'
import * as A from 'fp-ts/Array'
import * as E from 'fp-ts/Either'
const someFunction = (a: number) => E.right(a);
const numbers = [1, 2, 3];
const res = pipe(
numbers,
A.reduce([], (init: any[], acc) => {
const value = pipe(
acc,
someFunction,
E.getOrElseW((error) => error)
);
return [...init, value];
}),
E.right
);
2.2.2. email validation
import {array} from 'fp-ts'
import * as E from 'fp-ts/Either';
const validateEmail = (email: string): E.Either<Error, string> => {
return email.includes('@')
? E.right(email)
: E.left(new Error(`wrong email: ${email}`));
};
it('using traverse case', () => {
const validEmails = ['my@email.com', 'you@email.com'];
const validatedData = array.traverse(E.Applicative)(validateEmail)(validEmails);
expect(validatedData).toEqual(E.right(validEmails));
});
- 이를 sequence 로 바꾸면 아래와 같습니다.
import { sequence } from 'fp-ts/lib/Array';
const res = validEmails.map(validateEmail);
const sequencedData = sequence(E.Applicative)(res);
- 간략히 fp-ts 에 소개글을 공유하겠습니다.
traverse(A)(xs, f) <-> sequence(A)(A.map(xs, f))
sequence(A)(xs) <-> traverse(A)(xs, identity)
3. accumulate error
- sequence, traverse 모두 multi instance 를 single 로 바꾸는 역할을 합니다. 또한 이 덕분에 우리는 같은 monodic type 을 하나로 모을 수 있었습니다.
- 다만 여기서 sequence, traverse 의 차이가 나옵니다. sequence 는 error type( left 등)을 모을 수 있습니다. 하지만 traverse 는 그렇지 못합니다.
- traverse 는 설계 자체가 short-circuit(=either 와 같은) 으로 되어있습니다. 물론 sequence 도 마찬가지이지만 sequence 는 validation applicative 를 활용하여 적재가능하게 변경 할 수 있지만 traverse 는 그렇지 못합니다.
4. 결론
- map 을 바로 사용할 수 있으므로 traverse 자체를 활용하는 것은 sequence 보다 더 좋아보인다.
- 다만 sequence 와 달리 traverse 는 short-circuit 이 적용되므로 서로 사용하는 목적이 명확히 구분되어야한다.
1. 결과물이 원자성을 갖어야 하는 경우(1 or 0) -> traverse
- 모든 validation이 되어야 가입을 할 수 있다 던지
- 모든 과목의 점수가 계산 되어야 평균을 계산한다던지 등