apply 알아보기

김장훈·2023년 3월 1일
0

fp-ts function 탐구

목록 보기
1/5

1. interface

export declare type Identity<A> = A

export declare const ap: <A>(fa: Identity<A>) => <B>(fab: Identity<(a: A) => B>) => Identity<B>
  • apply 모양 자체가 상당히 이상한데 부분 부분 뜯어보겠습니다.
    😨 Identity 의 경우 사용된 타입을 그대로 전달해주는 역할인데 이를 굳이 왜 사용해주는지는 아직도 모르겠습니다. 제너릭을 그대로 사용해도 될것 같은데... 이유를 깨닫게 되면 업데이트 하겠습니다.

1.1 기본 구조

  • apply 는 closure 입니다. 그렇기에 크게 2가지 형태로 구분해서 보시면 이해하기 편합니다.

1.1.1 signature 를 받아서 가지고 있는다.

const ap: <A>(fa: Identity<A>) => fa
  • 제너릭에 변수명까지 해서 뭔가 복잡해보이지만 간단합니다.
  • ap 는 signature 로 받는 인자의 타입을 받아서 가지고 있습니다.
    it('example ap', () => {
      const sampleAp = <A>(fa: A) => fa;

      expect(typeof sampleAp('kk')).toEqual('string');
      expect(typeof sampleAp(1)).toEqual('number');
      expect(typeof sampleAp(() => '')).toEqual('function');
    });
  • 그렇기에 위에 처럼 ap 는 주어진 인자를 갖고 있다(현재는 예시를 위해서 return) 로 이해하면 됩니다.

1.1.2. 처음 받은 signature 를 사용하는 function 을 받는다.

// 아래 코드는 문법적으로 맞지 않습니다. 단순히 분석용을 위해 짜른것 입니다.
const sample = <B>(fab: Identity<(a: A) => B>) => Identity<B>
  • 위 코드를 보면 Identity<(a: A) => B> 부분이 있습니다. 제너릭으로 사용되어야 하는 부분이 function interface type 이 들어가있습니다.
  • 즉 fab 자리에 있는 signature 는 첫번째로 함수가 되어야 하고, 두번째론 A(처음에 받은 signature) 를 받아서 B 를 return 하는 형태가 되어야한다는 것 입니다.
export declare const ap: <A>(fa: Identity<A>) => <B>(fab: Identity<(a: A) => B>) => Identity<B>
  • 따라서 위 코드를 다시 보자면 첫 sinature 를 받고, 이 singature 를 활용해서 무언가 하는 function 을 return 하는 것 입니다.
  • 말 보다는 code 가 더 쉬우니 code 로 확인해봅시다.

1.1.3. code 예시

    it('should return function', () => {
      const sample = ap('my-name');
      expect(typeof sample).toBe('function');
    });
  • ap 는 string type 을 받았습니다. string type 을 처리하는 함수가 return 될 것이므로 sample 은 function type 이어야합니다.
  • 또한 func 의 interface 를 확인하면 아래와 같습니다.
const func: <B>(fab: (a: string) => B) => B
  • 즉 sample 는 function 을 받아야 하는데 제약 조건이 있습니다.
    • 해당 function 은 string 을 받아야 한다.
    it('should return function', () => {
      const sample = ap('my-name');
      expect(typeof sample).toBe('function');

      sample((a: string) => 0); // 첫번째
      sample((a: number) => 0); // 두번째
    });
  • 위에 처럼 작성하게 되면 두번째는 ts 에서 에러를 뱉어내게 됩니다.
Argument of type '(a: number) => typeof number' is not assignable to parameter of type '(a: string) => typeof import("~/node_modules/fp-ts/lib/number")'.
  Types of parameters 'a' and 'a' are incompatible.
    Type 'string' is not assignable to type 'number
  • 최초 받은 signature 가 string 이므로 string 을 받아야 하기 때문이죠.
    it('should return function', () => {
      const sample = ap('my-name');
      expect(typeof sample).toBe('function');

      sample((a: string) => 0);

      const sample2 = ap(0);
      sample2((a: number) => 0);
    });
  • 그렇기에 signature 를 number 로 바꾸면 문제가 사라집니다.

2. 어디에 쓰는가?

  • 제일 중요한 문제입니다. 어디에 쓰면 좋을까요?

2.1. 일반적인 사용방식

  • 보통은 curried 된 function 을 pipe 에 사용하기 위해 씁니다.
    it('use as curried function', () => {
      const upperCaseTwoStrings = (char: string) => (char2: string) => {
        return `${char.toUpperCase()} ${char2.toUpperCase()}`;
      };

      const expected = 'HELLO WORLD';
      expect(upperCaseTwoStrings('hello')('world')).toEqual(expected);

      const res = pipe('HELLO', upperCaseTwoStrings); // pipe('HELLO', 'WORLD', upperCaseTwoString)
      expect(res).not.toEqual(expected);
      expect(res('WORLD')).toEqual(expected);
    });
  • pipe 를 curried function 에 사용하게 될 경우 결과 값이 아닌 function 이 또 나오기 때문에 의도한 형태가 되지 않습니다(> res('WORLD'))
    (관련 좋은 문서)
  • upperCaseTwoString 를 보면 A, B 를 연달아 받아서 무언가 하는 형태이므로 ap 를 적용하기 좋습니다.
    it('use as curried function', () => {
      const upperCaseTwoStrings = (char: string) => (char2: string) => {
        return `${char.toUpperCase()} ${char2.toUpperCase()}`;
      };

      const expected = 'HELLO WORLD';
      expect(upperCaseTwoStrings('hello')('world')).toEqual(expected);

      const res = pipe('HELLO', upperCaseTwoStrings); // pipe('HELLO', 'WORLD', upperCaseTwoString)
      expect(res).not.toEqual(expected);
      expect(res('WORLD')).toEqual(expected);

      const res2 = pipe(upperCaseTwoStrings, ap('hello'), ap('world'));
      expect(res2).toEqual(expected);

      const res3 = pipe(upperCaseTwoStrings, ap('HHH'));
      const res4 = ap('HHH')(upperCaseTwoStrings);

      const res5 = pipe(upperCaseTwoStrings, ap('hello'), ap('world'));
      const res6 = ap('world')(ap('hello')(upperCaseTwoStrings));
      expect(res5).toEqual(res6);
    });
  • res2 를 보면 pipe(... ) 로 해서 curried function 을 사용할 수 있도록 변경 되었습니다.
  • 다만 기존 pipe의 형태: pipe(data, ...funcs) 가 아닌 모양이라서 다소 혼동이 될 수 있기에 아래 풀어진 모양도 넣었습니다.
  • 사용법이 pipe 랑 다르기에 헷갈릴만 하지만 오히려 순서만 보면 기존의 function 을 사용하는 것과 같은 순서라는 점도 흥미롭습니다( function(args1, args2)
    • 즉 pipe의 signature 순서가 바뀌게 되었습니다
    • ASIS: data, function, function ...
    • TOBE: function, data, data
  • 다만 curried function 을 그렇게 많이 사용하지 않다보니 위와 같은 ap 사용법은 와닿지는 않네요.

2.2. 다른 Case

  • 원래 pipe 의 순서가 바뀌는 것을 보고 이를 flow 에도 적용할 수 있지 않을까 라는 생각으로 글을 작성했지만 막상 해보니 flow 에는 ap 를 사용하지 쉽지 않아보입니다.
  • 그러므로 ap 를 사용하는 유용한 case 는 차후 업데이트 하도록 하겠습니다.
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

0개의 댓글