Traverse 알아보기

김장훈·2023년 4월 6일
0

fp-ts function 탐구

목록 보기
4/5

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[]>
  1. Applicative 를 받습니다. value 를 lift 하기 위해서입니다. 이는 sequecne 에서 사용하는 형태와 동일합니다. 이후 function 을 return 합니다.
  2. function 을 sigature 로 받습니다. 저 모양 자체는 map 사용처랑 흡사합니다(단지 해당 function 의 return 이 HKT type 인것만 제외하면)
  3. 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) // E.right([1,2,3])
  • 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) // E.right([1,2,3])
  • 뭔가 엄청난 차이가 있어야 할 것 같지만 사실 그렇지 않습니다.
  • 결국 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
); // E.right([1,2,3])

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이 되어야 가입을 할 수 있다 던지
    • 모든 과목의 점수가 계산 되어야 평균을 계산한다던지 등
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

0개의 댓글