abonglog logoabonglog

Chapter 04: Currying [번역] 의 썸네일

Chapter 04: Currying [번역]

mostly-adequate-guide
프로필 이미지
yonghyeun

해당 게시글은 mostly-adequate-guide 의 시리즈들을 번역했습니다.

해당게시글의 저작권은 mostly-adequate-guide 의 저작권인 Attribution-ShareAlike 4.0 International 을 준수합니다.

🤖 AI가 요약한 글이예요 !
이 글은 함수형 프로그래밍 기법인 커링(currying) 에 대해 설명합니다.
커링은 함수를 예상보다 적은 인수로 호출했을 때, 나머지 인수를 받는 새로운 함수를 반환하는 방식입니다.
이를 통해 함수를 단계적으로 적용하거나, 인수를 미리 채워 넣어 재사용 가능한 함수를 쉽게 만들 수 있습니다.
curry 헬퍼 함수를 사용하여 커링된 함수를 정의하고 사용하는 예시를 보여주며, 데이터 인수를 마지막에 배치하는 패턴의 유용성을 강조합니다.
커링은 코드 중복을 줄이고, 함수를 더 유연하게 조합하며, 수학적 함수의 정의(단일 입력, 단일 출력)를 유지하는 데 도움이 됩니다.

챕터 04: 커링

당신 없이는 살 수 없다면 살 수 없어요

아버지께서 한번은 어떤 것들은 그것들을 얻기 전까지는 없이 살 수 있다고 설명해주신 적이 있습니다. 전자레인지가 그런 것 중 하나입니다. 스마트폰도 마찬가지죠. 우리 중 나이가 많은 분들은 인터넷 없이도 만족스러운 삶을 기억하실 겁니다. 저에게는 커링(currying) 이 이 목록에 있습니다.

커링(Currying): 여러 인수를 받는 함수를, 각각 단일 인수를 받는 함수들의 연속으로 변환하는 기법입니다. 예를 들어, f(a, b, c)f(a)(b)(c) 형태로 바꾸는 것입니다.

개념은 간단합니다: 함수가 기대하는 것보다 적은 인수로 함수를 호출할 수 있습니다. 그러면 나머지 인수를 받는 함수를 반환합니다. 한 번에 모든 인수를 호출하거나 각 인수를 조금씩 넣어줄 수 있습니다.

커링된 덧셈 함수 예시
const add = (x) => (y) => x + y;
const increment = add(1); // y => 1 + y
const addTen = add(10); // y => 10 + y
 
increment(2); // 3
addTen(2); // 12

여기서 우리는 하나의 인수를 받아 함수를 반환하는 add 함수를 만들었습니다. 이 함수를 호출함으로써, 반환된 함수는 클로저를 통해 첫 번째 인수를 기억합니다. 하지만 두 인수를 한 번에 호출하는 것은 약간 번거롭기 때문에, curry라는 특별한 헬퍼 함수를 사용하여 이런 함수들을 더 쉽게 정의하고 호출할 수 있습니다.

즐거움을 위해 몇 가지 커링된 함수를 설정해 봅시다. 지금부터는 부록 A - 필수 함수 지원에 정의된 curry 함수를 사용할 것입니다.

다양한 커링된 함수 정의
// curry 함수는 Appendix A - Essential Function Support 에 정의되어 있다고 가정합니다.
// const curry = require('./support'); // 예시: 실제 경로에 맞게 수정 필요
 
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));

제가 따른 패턴은 간단하지만 중요합니다. 우리가 작업하는 데이터(문자열, 배열)를 전략적으로 마지막 인수로 배치했습니다. 사용하는 즉시 그 이유가 명확해질 것입니다.

(구문 /r/g는 모든 'r' 문자와 일치하는 정규 표현식입니다. 원한다면 정규 표현식에 대해 더 읽어보세요.)

커링된 함수 사용 예시
match(/r/g, "hello world"); // [ 'r' ]
 
const hasLetterR = match(/r/g); // s => s.match(/r/g)
hasLetterR("hello world"); // [ 'r' ]
hasLetterR("just j and s and t etc"); // null
 
filter(hasLetterR, ["rock and roll", "smooth jazz"]); // ['rock and roll']
 
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(s => s.match(/r/g))
removeStringsWithoutRs(["rock and roll", "smooth jazz", "drum circle"]); // ['rock and roll', 'drum circle']
 
const noVowels = replace(/[aeiou]/gi); // (replacement, s) => s.replace(/[aeiou]/ig, replacement)
const censored = noVowels("*"); // s => s.replace(/[aeiou]/ig, '*')
censored("Chocolate Rain"); // 'Ch*c*l*t* R**n'

여기서 보여주는 것은 함수에 인수를 하나 또는 두 개 "미리 로드"하여 해당 인수를 기억하는 새 함수를 받는 능력입니다.

Mostly Adequate 저장소를 복제(git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git)하고, 위의 코드를 복사하여 REPL에서 시도해 보시기를 권장합니다. curry 함수와 부록에 정의된 모든 것은 support/index.js 모듈에서 사용할 수 있습니다.

또는 npm에 게시된 버전을 살펴보세요:

npm에서 support 라이브러리 설치
npm install @mostly-adequate/support

단순한 말장난 이상 / 특별한 소스

커링은 많은 것에 유용합니다. hasLetterR, removeStringsWithoutRs, censored에서 보았듯이 기본 함수에 몇 가지 인수를 제공하여 새 함수를 만들 수 있습니다.

또한 단일 요소에서 작동하는 모든 함수를 map으로 감싸서 배열에서 작동하는 함수로 변환할 수 있습니다:

map과 커링을 이용한 함수 변환
const getChildren = (x) => x.childNodes;
const allTheChildren = map(getChildren); // xs => xs.map(x => x.childNodes)

함수에 예상보다 적은 인수를 제공하는 것을 일반적으로 부분 적용(partial application) 이라고 합니다. 함수를 부분 적용하면 많은 상용구 코드를 제거할 수 있습니다. 위의 allTheChildren 함수가 lodash의 커링되지 않은 map (인수 순서가 다름에 유의) 으로 어떻게 될지 생각해 보세요:

부분 적용(Partial Application): 함수의 일부 인수를 미리 고정하여 더 적은 수의 인수를 받는 새로운 함수를 만드는 기법입니다. 커링과 유사하지만, 커링은 항상 단일 인수를 받는 함수들의 연속으로 변환하는 반면, 부분 적용은 여러 인수를 한 번에 고정할 수 있습니다.

커링되지 않은 map 사용 시 비교
// lodash의 map 함수 사용 예시 (인수 순서 다름)
// const _ = require('lodash');
// const allTheChildren = (elements) => _.map(elements, getChildren);

우리는 일반적으로 배열에서 작동하는 함수를 정의하지 않습니다. 왜냐하면 map(getChildren)을 인라인으로 호출할 수 있기 때문입니다. sort, filter 및 기타 고차 함수(higher-order function) 도 마찬가지입니다.

고차 함수(Higher-Order Function): 함수를 인수로 받거나 함수를 결과로 반환하는 함수입니다. map, filter, reduce 등이 대표적인 예입니다.

순수 함수에 대해 이야기할 때, 우리는 그것들이 1개의 입력을 받아 1개의 출력을 낸다고 말했습니다. 커링은 정확히 이것을 합니다: 각 단일 인수는 나머지 인수를 기대하는 새 함수를 반환합니다. 그것이 바로, 친구여, 1개의 입력에서 1개의 출력입니다.

출력이 다른 함수이든 상관없이 순수 함수 자격을 갖습니다. 한 번에 둘 이상의 인수를 허용하지만, 이는 단순히 편의를 위해 추가 ()를 제거하는 것으로 간주됩니다.

요약하자면

커링은 편리하며 저는 매일 커링된 함수로 작업하는 것을 매우 즐깁니다. 이것은 함수형 프로그래밍을 덜 장황하고 지루하게 만드는 도구입니다. 몇 가지 인수를 전달하는 것만으로 즉시 새롭고 유용한 함수를 만들 수 있으며, 보너스로 여러 인수에도 불구하고 수학적 함수 정의를 유지했습니다.

또 다른 필수 도구인 compose를 습득해 봅시다.


연습 문제

연습 문제에 대한 참고 사항

책 전반에 걸쳐 이와 같은 '연습 문제' 섹션을 만날 수 있습니다. gitbook에서 읽고 있다면 (권장) 브라우저에서 직접 연습 문제를 수행할 수 있습니다.

책의 모든 연습 문제에 대해 항상 전역 범위에서 사용할 수 있는 몇 가지 헬퍼 함수가 있다는 점에 유의하세요. 따라서 부록 A, 부록 B, 부록 C에 정의된 모든 것을 사용할 수 있습니다! 그리고 그것만으로는 충분하지 않다는 듯이 일부 연습 문제에서는 제시된 문제에 특정한 함수도 정의합니다. 사실상 그것들도 사용할 수 있다고 생각하세요.

힌트: 내장 편집기에서 Ctrl + Enter를 눌러 솔루션을 제출할 수 있습니다!

자신의 컴퓨터에서 연습 문제 실행하기 (선택 사항)

자신의 편집기를 사용하여 파일에서 직접 연습 문제를 수행하려는 경우:

  1. 저장소를 복제합니다 (git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git).
  2. 연습 문제 섹션으로 이동합니다 (cd mostly-adequate-guide/exercises).
  3. 권장 노드 버전 v10.22.1을 사용하고 있는지 확인합니다 (예: nvm install). 책의 readme에 이에 대한 자세한 내용이 있습니다.
  4. npm을 사용하여 필요한 종속성을 설치합니다 (npm install).
  5. 해당 챕터 폴더의 exercise_*라는 이름의 파일을 수정하여 답을 완성합니다.
  6. npm으로 수정을 실행합니다 (예: npm run ch04).

단위 테스트가 답에 대해 실행되고 실수 시 힌트를 제공합니다. 참고로 연습 문제의 답은 solution_*이라는 이름의 파일에서 사용할 수 있습니다.


연습해 봅시다!

다음 함수를 고려하십시오:

keepHighest 함수 정의
const keepHighest = (x, y) => (x >= y ? x : y);