Pipe 사용기 - 1

김장훈·2023년 8월 27일
0

Pipe 란

  • 여러개의 함수를 합성하여 사용할 수 있게 하는 helper function
  • D(C(B(A()))) 와 같은 중첩 함수를 사람이 읽기 쉽게 해준다
    (compose 와 함께 FP 패러다임에서의 꽃이라 생각)
pipe('some_data', A, B, C, D)

Pipe 사용 예시

export interface UserEntity {
  id: number;
  gender: 'F' | 'M';
  name: string;
}
export function getUsers(): UserEntity[] {
  return [
    {
      id: 1,
      gender: 'F',
      name: 'jane park',
    },
    {
      id: 2,
      gender: 'M',
      name: 'ivan kim',
    },
    { id: 10, gender: 'F', name: 'oll lee' },
  ];
}

const firstNames = pipe(
      getUsers(),
      A.map((data) => data.name),
      A.map((name) => name.split(' ')[1])
    );
  • users 를 가져와서 그 중에서 name 만 가져오고 name 에서 firstName 을 parsing 하는 로직이다.
  • for 문이나 변수 재할당 등 기타 불필요한 code(line) 이 존재하지 않으며 중간의 함수들만 변경하면 원하는 값을 가져올 수 있게 변경할 수 있으므로 유지보수에도 좋다고 생각한다.

내가 겪은 Pipe 단점(문제점)

  • pipe 를 사용하면서 겪은 단점은 바로 사용하는 함수의 signature 의 갯수에 따라 해야하는 것들이 많아진다는 것이다.
  • pipe 의 원리는 함수들의 return type 을 다음 함수들이 사용하는 channing 형태 이며 그렇기에 기본적으로 단항 함수들로 구성이 되어야한다.
export interface UserEntity {
  id: number;
  gender: 'F' | 'M';
  name: string;
}
export function getUsers(): UserEntity[] {
  return [
    {
      id: 1,
      gender: 'F',
      name: 'jane park',
    },
    {
      id: 2,
      gender: 'M',
      name: 'ivan kim',
    },
    { id: 10, gender: 'F', name: 'oll lee' },
  ];
}

export function filterGender(gender: 'F' | 'M', users: UserEntity[]) {
  return users.filter((user) => user.gender == gender);
}
  • 가령 위와 같은 형태에서 getUsers, filterGender 를 pipe 로 구성한다고 할 경우 아래와 같이 사용할 수 없다.
// wrong case 1
// Argument of type 'UserEntity[]' is not assignable to parameter of type '"F" | "M"'.ts(2345)
pipe(
  getUsers(), 
  filterGender);

// wrong case 2
// Argument of type 'UserEntity[]' is not assignable to parameter of type '(a: UserEntity[]) => unknown'.
//  Type 'UserEntity[]' provides no match for the signature '(a: UserEntity[]): unknown'.ts(2345)
Expected 2 arguments, but got 1.ts(2554)
pipe(
  getUsers(), 
  filterGender('M'));

wrong case 1

  • getUsers의 return 값은 은 바로 filterGender 에 들어가야하는데 filterGender 의 interface 와 맞지 않다(gender, users 이므로)

wrong case 2

  • 그렇다면 gender 를 주면 되지 않을까? 이지만 interface 의 갯수가 충분치 않으므로 syntax 에러가 발생한다.
    (또한 pipe 에는 실행되지 않은 형태의 함수가 주어져야하므로 interface 갯수가 맞는다 하더라도 실행은 불가능 하다)

  • 실제 우리가 현업에서 구현하는 함수들은 다항 함수인 경우가 대부분이다. 그렇기에 pipe 를 현업에서 사용하는 것은 이러한 부분들을 고려해야한다. 그렇다면 어떻게 해야할까?

Pipe 해결방법(일부)

더 좋은 방법을 계속 찾고 있는 중 ...

  • 결국 핵심은 다항 함수를 단항 함수로 변경하는 것이다. 그리고 이러한 목표를 위해서 사용할 수 있는 방법은 명확히 존재한다.

currying

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument
(wiki)

  • 커링은 다항함수를 단항함수로 바꾸는 가장 쉬운 방법이다.
const filterMaleGender = (users: UserEntity[]) => filterGender('M', users);
pipe(
  getUsers(), 
  filterMaleGender);
  • filterGender 에 'M' 을 이미 주는 형태로 해서 currying 된 함수를 구현하고 이를 사용하였다. 그 결과 2개의 다항 함수가 1개의 단항 함수로 변경 되었다.
  • 3,4 개의 다항 함수라도 내가 사용할 1개의 signature 를 제외한 나머지를 미리 받아놓는 형태로도 사용할 수 있다.
  • pipe 내의 함수들만 보면 무엇을 하려는지 명확하게 알 수 있다.
  • 다만 위와 같은 형태를 선뜻 적용할 수 없던 이유는 함수가 너무 많아졌기 때문이다.
  • currying 된 함수가 다른 곳에서도 사용이 되면 좋겠지만 그렇지 않은 경우가 더 많았고 그 결과 함수 내에 함수로 많아지다 보니 읽는게 더 어려워진다 는 생각이 많이 들었다.

direct currying

  • 다항 함수를 변경하는 방법은 currying 형태 밖에 없다. 다만 이를 어떻게 사용하는지 그 방법만 변경하였다.

pipe(
  	getUsers(), 
  	(users: UserEntity[]) => filterGender('M', users));
  • 사실상 이전에 언급한 방법과 다른건 없다. 다만 불필요한 함수가 생성은 되지 않는다.
  • 하지만 pipe 내에 구체적인 구현부가 노출 되어서 가독성이 떨어진다.

결론

  • 한개의 함수에서 하나의 역할을 한다는 측면에선 당연히 다항함수보단 단항함수가 훨씬 좋다. 하지만 현실은 교과서 같지 못하다...
  • 아직도 어떠한 방법이 좋은지 결정짓지 못했다. 다만 코드양이 늘어나는 것은 지양해야한다고 생각하기에 현재는 두번째 방법을 활용하고 있다.
  • 더 좋은 방법이 있다면 업데이트 예정 ...
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

0개의 댓글