(번역)Practical Guide to Fp-ts P1: Pipe and Flow

김형태·2024년 1월 31일
0

fp-ts

목록 보기
1/4

원문: Practical Guide to Fp-ts P1: Pipe and Flow - Ryan's Blog

Fp-ts를 사용하는 방법을 알아봅시다. pipe와 flow 연산자에 대해 소개할 것입니다. 수학적인 지식은 없어도 됩니다.

Introduction

이 글은 타입스크립트를 위한 함수형 프로그래밍 라이브러리인 fp-ts에 대해 소개하는 글입니다. 왜 fp-ts를 배워야 할까요? 첫 번째 이유는 더 나은 타입 안정성(type safety)입니다. Fp-ts는 사용자 정의 타입 가드as 연산자 없이 데이터 구조에 대한 단언을 가능하게 합니다. 두 번째 이유는 표현력과 가독성입니다. Fp-ts는 실패할 수도 있는 일련의 연산자들을 우아하게 모델링하는 데에 필요한 도구들을 제공합니다. 전체적으로 여러분이 다룰 수 있는 기술에 fp-ts를 추가하면 좋을 것입니다. 더 나은 타입스크립트 프로그램을 작성하는데 도움이 되기 때문입니다.

대중적인 의견과는 달리, 함수형 프로그래밍을 배우기 위해 복잡한 수학을 이해할 필요는 없습니다. 각 연산자가 어떻게 동작하는제 느낌만 알면 됩니다. 기본 연산자들을 이해하고 나면 돌아가서 수학을 다시 보면 됩니다. 이를 염두에 두고 이 게시물과 다음 게시물에서는 필요하지 않는 한 수학적 전문 용어를 피하고 실용적인 관점에서 함수형 프로그래밍을 소개하겠습니다.

The Pipe Operator

fp-ts의 기본적인 구성 요소는 Pipe 연산자입니다. 직관적으로 연산자를 사용하여 왼쪽에서 오른쪽으로 일련의 함수를 연결할 수 있습니다. pipe의 타입 정의는 임의 개수의 인수를 사용합니다. 첫 번째 인수는 임의의 값일 수 있고, 이후의 인수는 각각 한 개의 매개변수를 갖는 함수(arity one이라고 돼있는데 적절한 번역인지 모르겠습니다..)여야 합니다. 파이프라인에 있는 앞에 있는 함수의 반환 유형은 뒤에 오는 함수의 입력 유형과 일치해야 합니다.

간단한 덧셈과 곱셈 함수를 pipe하는 방법을 살펴보겠습니다.

import { pipe } from 'fp-ts/lib/function'

function add1(num: number): number {
  return num + 1
}

function multiply2(num: number): number {
  return num * 2
}

pipe(1, add1, multiply2) // 4

연산의 결과는 4입니다. 어떻게 결과에 도달해게 됐을까요? 단계적으로 살펴보겠습니다.

  1. 1로 시작합니다.
  2. 1이 파이프로 연결된 add1의 인수로 전달되고, add1을 더해 2로 평가됩니다.
  3. add1의 반환 값인 2multiply2의 인수로 전달되고, 2를 곱해 4로 평가됩니다.

지금 우리의 파이프라인은 number를 입력받아 새로운 number를 출력합니다. 입력받는 타입을 다른 타입(ex. string)으로 변환하는 것이 가능할까요? 정답은 '예'입니다. 파이프라인의 마지막에 toSTring 함수를 추가해봅시다.

function toString(num: number): string {
  return `${num}`
}

pipe(1, add1, multiply2, toString) // '4'

이제 우리의 파이프라인은 '4'로 평가됩니다. 만약 우리가 toStringadd1multiply2 사이에 뒀다면 어떻게 될까요? 우리는 컴파일 에러를 얻게 됩니다. toString의 출력 타입인 stringmultiply2의 입력 타입인 number와 일치하지 않기 때문입니다.

pipe(1, add1, toString, multiply2)

Argument of type '(num: number) => string' is not assignable to parameter of type '(b: number) => number'. Type 'string' is not assignable to type 'number'.ts (2345)

요약하자면, 여러분은 특정한 값을 일련의 함수들을 통해 변환하기 위해 pipe 연산자를 사용할 수 있습니다. 제어 흐름은 다음과 같이 모델링 될 수 있습니다.

A -> (A->B) -> (B->C) -> (C->D)

The Flow Operator

flow 연산자는 pipe 연산자와 거의 유사합니다. 차이점은 첫 번째 인수가 숫자와 같은 임의의 값이 아니라 함수여야 한다는 것입니다. 첫 번째 함수는 또한 하나 이상의 매개변수를 가질 수 있습니다.

예를 들면, 위에서 작성했던 세 개의 함수를 flow 연산자에 넣을 수 있습니다.

import { flow } from 'fp-ts/lib/function'

pipe(1, flow(add1, multiply2, toString))
flow(add1, multiply2, toString)(1) // 위와 동일합니다.

pipe 연산자와 비교해보면, 제어의 흐름(flow)은 다음이 나타낼 수 있습니다.

(A->B) -> (B->C) -> (C->D) -> (D->E)

flow 연산자의 좋은 용례는 무엇일까요? 언제 pipe 대신 flow 연산자를 사용할 수 있을까요? flow 연산자는 익명함수를 피하고자 할 대 특히 유용합니다. 타입스크립트에서 익명함수의 좋은 예는 콜백입니다.

인수가 2개인 concat 함수를 정의해봅시다. 첫 번째 인수는 숫자일 것이고, 두 번째 인수는 첫 번째 인수로 넘어온 숫자를 인수로 받아 문자열로 바꾸는 콜백함수일 것입니다. concat 함수는 첫 번째 값으로 첫 번째 인수를, 두 번째 값으로 콜백에 의해 변환된 값을 가지는 2차원 튜플을 반환합니다.

function concat(
  a: number,
  transformer: (a: number) => string,
): [number, string] {
  return [a, transformer(a)]
}

앞에 사용했던 함수를 concat 함수의 콜백으로 사용할 수 있습니다. 다음은 pipe 연산자를 사용한 예시입니다.

concat(1, (n) => pipe(n, add1, multiply2, toString)) // [1, '4']

이와 같이 사용했을 때의 문제점은, pipe 연산자와 함께 사용하기 위해 n을 익명 함수의 일부로 선언해야 한다는 것입니다. 이를 사용하면 외부 범위의 변수를 가려서(shadowing) 오류의 위험성이 있으므로 이를 피해야 합니다. 또한 이는 더 많은 양의 코드를 필요로 하는 더욱 번잡한 방식입니다.

해결책은 flow 연산자를 사용해서 익명 함수를 제거하는 것입니다. 이는 flow의 반환 값이 함수 자체이기 때문에 작동합니다. 이 함수의 시그니처는 number -> string으로, 정확히 콜백의 시그니처와 일치합니다.


Appendix

profile
steady

0개의 댓글