함수형 프로그래밍 - 순수함수의 합성편 |> Pipe

teo·2021년 12월 1일
13
post-thumbnail

Previous

지난 글에서는 왜 프론트엔드 개발자가 함수형 프로그래밍을 해야 하는지에 대해서 얘기를 해봤습니다.

요약하자면,

1) javascript는 태생이 함수형 언어를 근간으로 해서 만들어져있다.
그래서 함수형 프로그래밍을 잘 하는 것 = javascript를 잘하는 것이다.

하지만 함수형 프로그래밍을 막상 배우고자 하면 객체지향만큼이나 배워야 할 패러다임이 많은데다가 javascript는 멀티패러다임 언어이기 때문에 완전히 순수 함수형 언어의 방식으로만 개발을 할 수도 없기 때문에 어설프게 알게되면 혼란을 겪게 됩니다.

그래서 함수형 프로그래밍을 본격적으로 deep dive하기 전에 앞서 바로 실천이 가능한 원칙을 언급했습니다.

2) 가능한 순수함수의 형태로 작성하라.

그리고 순수 함수의 형태로 작성하기 위해서 습관적으로 해야하는 좋은 코딩 습관들을 공유 했습니다.

3) let를 가급적 쓰지 말고 const를 써라.
4) Array, Date의 Mutation 함수를 가급적 쓰지 마라. (push, pop, reverse, splice, ...)
5) Object의 필드 대입을 가급적 쓰지 마라.

이러한 좋은 습관들을 지키기가 어렵나요? 그렇다면 강제로 지킬 수 있게 해주는 좋은 도구인 ESLint를 써보기를 권해드립니다.
ESLint에 대해서는 다시 한번 정리해서 다음번 주제로 작성을 할 예정이며,

이번 글에서는 지난 글에서 언급했던 순수함수의 합성편에 대해서 알아보도록 하겠습니다.

들어가기 앞서...

실제로 코딩의 세계에서는 이론보다는 실전이 훨씬 더 중요합니다. 내가 설사 개념이나 용어를 잘 모른다고 하더라도 습관처럼 좋은 개발을 하고 있는 사람들도 있고 이론은 잘 알고 있는데 정작 본인의 코드에서는 그러한 부분들이 전혀 발견되지 못하는 사람들도 있습니다.

그래서 저는 모든 이론적 베이스를 전달하지 않고 최대한 실전성이 있는 내용 위주로 작성을 하고 싶지만 그래도 키워드등을 설명하지 않으면 정작 더 깊이 파고 싶을때 검색어를 어떻게 해야 하는지 모르는 경우가 생기 때문에 최소한의 키워드 위주의 설명만 포함하려고 하며 때로는 일부러 소개를 하지 않는 용어나 개념들도 있기 때문에 함수형 프로그래밍의 감이 잡히신다면 정석도 꼭 한번 공부 해 보시길 바랍니다.

1. 순수함수 + 순수함수 = 순수함수! - 함수의 합성

지난 시간의 핵심은 가능하면 순수함수로 작성을 하라. 순수함수는 사이드이펙트가 없기 때문에 테스트 하기 용이하고 코드를 간결하게 만들어준다고 했습니다. 그리고 순수함수와 순수함수의 경우 결합을 하더라도 순수함수가 되기 때문에

값 => (순수함수 => 순수 함수 => 순수 함수 => 순수 함수) => 비 순수 함수

와 같은 형태로 코딩을 하여 사이드 이펙트가 날 수 있는 비 순수 함수의 구간을 줄인다고 했습니다.

그렇다면 순수함수는 어떻게 결합을 할 수 있을까요? f(x)g(x)가 있다면 g(f(x))와 같은 형태로 합성을 할수 있겠습니다.

간단한 예시를 들어봅시다.

문자열을 입력받아서 공백을 제거하고 전부 소문자로 만들고 첫글자를 대문자로 표기한 다음 !를 붙이는 프로그램을 작성하시오.
ex) " hElLo, worLd" => "Hello, world!"

일반적으로 절차형 프로그래밍으로 간단히 구현해 봅시다.

const makeStr = (str) => {
  let result = str.trim()
  result = result.toLowerCase()
  result = result.charAt(0).toUpperCase() + str.slice(1)
  result = result + "!"
  return result
}  

함수형 프로그래밍의 방식대로 순수함수로 나눠보고, 전통적인 방식으로 합성을 해봅시다.

const trim = (str) => str.trim()

const lowerCase = (str) => str.toLowerCase()

const captitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)

const wow = (str) => str + "!"


const makeStr = (str) => wow(captitalize(lowerCase(trim(str)))

🤔 ...

뭔가 코드가 정돈 된 것 같기도 하고 굳이 이렇게 해야 하나 싶고 특히 함수를 합성된 형태를 보면 현실성있어 보이는 코드가 아닌 것 같은데 어떠신가요?

특히 마지막에 wow(captitalize(lowerCase(trim(str))) 이런식으로 합성의 단위가 2개 이상이 되면 실제 현실감각에서의 순서와 역순으로 배치가 되기 때문에 상당히 불편합니다.

그래서 만약 순서대로

const result = "  hElLo, worLd" |> trim |> lowerCase |> captitalize |> wow // "Hello, world!"

이런식으로 개발을 할 수 있다면 어떨까요?

이렇게 순서대로 작성을 할 수 있다면 각 함수 블록들은 순수함수로 되어 있어서 적당히 순서대로 조립을 하면 사이드 이펙트가 없는 코드를 작성하는 함수형 프로그래밍을 할 수 있을 것 같네요.

NOTE: 자바스크립트에서는 이러한 방식으로 개발하기 위해 |> 와 같은 operator를 만들자고 제안을 했지만 아직도 계류중인 상태입니다.
https://github.com/tc39/proposal-pipeline-operator

이러한 operator없이 javascript만으로는 계속 불편하게 역순으로 합성을 하는 방법 밖에 없을까요?

아닙니다!

javascript는 이미 함수형 프로그래밍을 할 수 있도록 설계된 언어라고 말씀 드렸습니다. 문법적으로 함수형 프로그래밍을 지원하지 않더라도 우리는 함수형 프로그래밍을 할 수 있습니다.

2. 우리는 이미 함수형 프로그래밍을 하고 있다. 일급 함수!

일급객체? 일급함수?

함수형 프로그래밍을 지원하는 언어가 되기 위해서는 필수적으로 만족해야 하는 조건이 있습니다. 바로 함수가 변수나 다른 함수의 인자나 return 값으로 사용하기 가능해야 합니다.

변수에 넣을 수 있고, 함수의 인자나 반환값이 될 수 있는 값을 일급 객체라고 하며 함수가 이러한 일급객체의 조건을 만족할 때 우리는 일급함수라고 부릅니다.

// 일급객체 - 문자열
const foo = "hello" // 변수에 값을 넣을 수 있다.

const makeWow = (str) => {
	const c = str + "!"
    retutn c // 반환값으로도 가능
}

const result = makeWow(foo) // 함수의 인자로도 사용 가능

console.log(result) // hello!
// 일급함수
const foo = "hello"

const makeWow = (str) => str + "!"

const otherWow = makeWow // 변수에 대입이 가능

const createWowFunction = (callback) => {
  return (str) => callback(str) + "?" // 함수를 반환값으로 사용가능
}

const result = createWowFunction(otherWow) // 함수를 인자값으로 사용가능

constole.log(result(foo)) // hello!?

addEventListener(...), Array.protype.map(...), Promise.then(...)

우리는 이러한 개념들을 이미 기초 프로그래밍을 할 때 사용했습니다. 지금 javascript를 비롯한 언어를 배우시는 주니어 개발자들은 너무나 당연한 개념일 수 있지만 주류언어 중에서 함수가 일급객체인 언어는 거의 없었습니다.

함수형 프로그래밍를 배운다는 것은 이러한 함수가 값이되고 인자가 되고 반환값이 될 수 있다는 성질을 응용하여 어떻게 활용되는지를 배우는 디자인 패턴과도 같다라고 볼 수 있습니다.

3. pipe함수를 만들어 보자.

다시 처음으로 돌아가 봅시다.

우리는 순수함수를 결합하기 위해서 함수를 여러번 실행하는 방식으로 하면 순서가 뒤집어 지기 때문에 사용하기가 불편하다고 했습니다. 그러면 어떻게 순서대로 작성하게 할 수 있을까요?

const trim = (str) => str.trim()

const lowerCase = (str) => str.toLowerCase()

const captitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)

const wow = (str) => str + "!"

// 어떻게 pipe를 구현할 수 있을까?
const pipe = (...) => { ... }

const pipeStr = (str) => pipe(str, trim, lowerCase, captitalize, wow)

pipe의 인자값은 위 예시를 보아 알겠지만 초기값을 받고 나머지 값들은 전부 함수를 받는다는 것을 알 수 있습니다.

그래서

const pipe = (value, ...fns) => { ... }

이러한 형태를 떠올릴수 있습니다.

그러면 다시 맨 처음 절차적 프로그래밍 예시를 떠올려 봅시다.

export const makeStr = (str) => {
  let result = str.trim()
  result = result.toLowerCase()
  result = result.charAt(0).toUpperCase() + str.slice(1)
  result = result + "!"
  return result
}  

result의 값에 계속 result를 대입해서 결과를 돌려준다는 것을 알게 되었습니다. 그러니

const pipe = (value, ...fns) => {
  for (let i = 0; i < fns.length; i++) {
    const fn = fns[i]
    value = fn(value)
  }
  return value
}  

이런식으로 쓸수 있을 것 같습니다.

Array의 reduce함수를 사용해서 조금더 간결하게 적어 보면

const pipe = (value, ...fns) => fns.reduce((value, fn) => fn(value), value)

로 만들게 되면,

const trim = (str) => str.trim()

const lowerCase = (str) => str.toLowerCase()

const captitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)

const wow = (str) => str + "!"

const pipe = (value, ...fns) => fns.reduce((value, fn) => fn(value), value)


const pipeStr = (str) => pipe(str, trim, lowerCase, captitalize, wow)

const pipeStr2 = (str) => pipe(str, captitalize, wow)

const pipeStr3 = (str) => pipe(str, trim, lowerCase)


console.log(pipeStr("  HelLo")) // "Hello!"
console.log(pipeStr2("  HelLo")) // "  HelLo!"
console.log(pipeStr3("  HelLo")) // "hello"

감이 오실까요?

이렇게 순수함수를 조립해서 코딩을 할 수 있는 함수형 프로그래밍 세계에 한발짝 들어오시게 된것을 환영합니다.

여기까지 내용을 통해 순수함수를 합성해서 프로그래밍을 만든다! 라는 감각을 이해하셨으라 생각합니다.

4. One More Thing... 진짜 pipe함수를 만들어 보자.

위 pipe 함수를 실전성이 있게 한 발만 더 나가 봅시다. 분명 처음에

순수함수 + 순수함수 = 순수함수

라고 함수를 만든다고 했지만 pipe의 합성은 잘 보시면 결과물이 함수가 아니라는 점에 주목해주세요.

위 pipe 함수는 결국 값을 만들어 내기 때문에 한번 조립된 pipeStrpipStr2를 다시 조립할 수가 없게 됩니다.

그래서 pipe의 함수가 함수를 반환할 수 있도록 재작성 해봅시다.

const pipe = (...fns) => (value) => fns.reduce((value, fn) => fn(value), value)

2중으로 화살표 함수를 쓰는 부분이 여기서 제일 헷갈릴 수 있는 부분인데 아래와 같이 풀어서 보시면 조금 더 이해가 되리라 생각합니다.

const pipe = (...fns) => { 
  return (value) => fns.reduce((value, fn) => fn(value), value)
}

자! 이제 우리는 pipe로 합성이 된 함수를 다시 함성을 할 수 있게 되었습니다.

// value를 받는 함수를 만들어 낸다.
const pipe = (...fns) => (value) => fns.reduce((value, fn) => fn(value), value)

const pipeStr = pipe(trim, lowerCase, captitalize, wow)

const pipeStr2 = pipe(captitalize, wow)

const pipeStr3 = pipe(trim, lowerCase)

// 이제 합성된 함수를 합성 할 수 있다.
const newPipeStr = pipe(pipeStr2, pipeStr3)

console.log(pipeStr("  HelLo")) // "Hello!"
console.log(pipeStr2("  HelLo")) // "  HelLo!"
console.log(pipeStr3("  HelLo")) // "hello"

console.log(newPipeStr("  HelLo")) // "hello!"

5. 찐찐막 pipe함수를 만들어 보자.

pipe의 함수는 이제 완성했습니다. 조금 더 실전성을 더 해서 한발짝만 더 나아가 봅시다.

조립되는 파이프라인에서 커스텀을 할 수 있도록 만들어 봅시다!

이중 함수를 이용해서 내부적으로 사용되는 값을 외부로 보내봅시다.

const _wow = (str) => str + "!"

const makePunct = (punct) => (str) => str + punct // wow를 커스텀하게 업그레이드 해보자!

const wow = makePunct("!")

const question = makePunct("?")


const pipe = (...fns) => (value) => fns.reduce((value, fn) => fn(value), value)

const pipeStr = pipe(trim, lowerCase, captitalize, wow, question)

console.log(pipeStr("  HelLo")) // "Hello!?"

조금더 응용을 하면 인라인 함수와 결합을 하면 다음처럼 쓸 수도 있습니다.

const pipeStr = pipe(
  str => str.trim(),
  str => str.lowerCase(),
  str => str + "!"
)

console.log(pipeStr("hello!"))

여기에 한 걸음을 더 나아가면 인자값을 함수로 넣는 방식으로도 진화할 수 있는데 이것은 다음 시간의 주제이므로 이번에는 여기까지 하도록 하곘습니다 ㅎㅎ

끝으로...

이번 글에서 알아야 할 내용에 대해서 요약하고 마무리 하곘습니다.

  • 함수형 프로그래밍은 함수를 값으로 인자로 반환값으로 쓸수 있다. 이것을 일급함수라고 부른다.
  • 순수함수끼리 합성하면 순수함수가 된다.
  • 순수함수를 조립하기 위해서는 고전적인 방식으로는 f(), g() => g(f()) 으로 사용한다.
  • 이렇게 만들면 순서가 뒤집어 지기 때문에 pipe(...) 방법이 필요하다.
  • 이것을 표준 문법인 f |> g 와 같이 operator를 제안했으나 아직은 계류중이다.

함수형 프로그래밍에서 함수 합성을 하는 방법 중에서 일급함수 그리고 이것을 이용한 pipe에 대해 알아보았습니다.

아직은 pipe만으로는 실전에 쓰기는 모자라니 감을 잡기 위한 일종의 기초적인 토대 정도라고 생각해주시면 될것 같아요.

본격적인 실전 편은 다음에 callback 편으로 찾아 뵙겠습니다.

좋은 하루 되세요 :)

profile
Svelte, rxjs, vite, AdorableCSS를 좋아하는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

8개의 댓글

comment-user-thumbnail
2021년 12월 1일

개발자님 엄청 세세하게 알려주셔서 감사합니다!!🙌 오늘도 많이 배우고 갑니다

1개의 답글
comment-user-thumbnail
2021년 12월 2일

익숙하면서 먼가 새롭네요.
그럼에도 아직 저에겐 절차형 프로그래밍이 가장 좋게 다가오네요.
일회성 기능의 함수화가 효율적인가?
wow를 보았을때 유추가 안되는것처럼 적절한 네이밍 고통이 따르고,
유추가 힘들 때 디버깅하는 번거로움이 예상되네요

1개의 답글
comment-user-thumbnail
2021년 12월 8일

좋은 글 감사합니다 :)

1개의 답글
comment-user-thumbnail
2021년 12월 12일

좋은 글 감사합니다. 무작정 타입스크립트 + 리액트에 입문하면서 비슷한 생각을 한 적이 있었는데, pipe 함수를 만드는건 생각을 못했네요! 다음에 한번 써먹어봐야겠습니다.ㅎㅎ

1개의 답글