04장: 커링
'커링' 없는 삶은 살 만한가?
아버지께서는 일단 갖고 나면 없이는 못 사는 것들이 있다고 하셨습니다. 전자레인지도 그렇고 스마트폰도 마찬가지죠. 연배가 높으신 분들은 인터넷 없이도 충만한 삶을 살았던 시절을 기억하실 겁니다. 저에게 있어 커링이 바로 그런 존재입니다.
개념은 단순합니다: 함수가 기대하는 인자 수보다 적은 인자로 호출할 수 있습니다. 이때 남은 인자를 받는 새 함수를 반환합니다.
한 번에 모든 인자를 전달하거나 조금씩 나눠서 제공할 수 있습니다.
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12여기서는 인자를 하나 받은 다음 새 함수를 반환하는 add 함수를 만들었습니다. 클로저를 통해 첫 번째 인자를 기억하게 되는 거죠. 하지만 두 인자를 한 번에 전달하는 방식은 번거로울 수 있으므로 이런 상황에 curry 헬퍼 함수를 사용하면 편리합니다.
실습을 위해 커링 함수 몇 개를 준비해 보겠습니다. 앞으로는 부록 A - 핵심 함수 지원에 정의된 curry 함수를 사용할 것입니다.
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); // x => x.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(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'이 예제는 인자 하나나 둘을 '사전 로딩'하여 해당 인자를 기억하는 새로운 함수를 생성하는 능력을 보여줍니다.
Git 저장소를 복제(git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git)하신 후 REPL에서 직접 코드를 실습해 보시길 권합니다. 부록에 정의된 커링 함수를 비롯한 모든 기능은 support/index.js 모듈에서 사용 가능합니다.
npm에 배포된 버전도 확인해 보세요:
npm install @mostly-adequate/support언어유희를 넘어선 특별한 조미료
커링은 다양한 상황에서 유용합니다. 기본 함수에 인자만 제공하면 hasLetterR, removeStringsWithoutRs, censored 같은 새로운 함수를 손쉽게 만들 수 있습니다.
map으로 감싸기만 하면 단일 요소 처리 함수를 배열 처리 함수로 변환할 수도 있습니다:
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);필요한 인자보다 적은 수를 제공하는 기법을 부분 적용이라고 합니다. 부분 적용을 사용하면 상용구 코드를 상당히 줄일 수 있습니다. lodash의 비커링 버전 map을 사용할 때의 코드를 비교해 보세요(인자 순서가 다름에 유의):
const allTheChildren = elements => map(elements, getChildren);보통 배열 전용 함수를 따로 정의하지 않는데, 인라인으로 map(getChildren)을 호출할 수 있기 때문입니다. sort, filter 같은 고차 함수(고차 함수는 함수를 인자로 받거나 반환하는 함수)도 마찬가지입니다.
순수 함수에 대해 논할 때 1입력 1출력 원칙을 언급했었죠. 커링은 각 단계에서 새로운 함수를 반환함으로써 이 원칙을 정확히 따릅니다. 친애하는 독자여, 이게 바로 1입력 1출력의 구현입니다.
반환값이 다른 함수라 해도 순수 함수의 조건을 충족합니다. 편의를 위해 여러 인자를 한꺼번에 전달할 순 있지만, 이는 단순히 추가적인 ()를 제거하기 위한 것일 뿐입니다.
요약
커링은 실무에서 매일같이 활용할 만큼 유용한 도구입니다. 함수형 프로그래밍의 반복 작업을 간소화하는 벨트 속 필수 아이템이죠.
몇 개의 인자만 전달해도 즉시 활용 가능한 새 함수를 생성할 수 있을 뿐만 아니라, 여러 인자를 처리하면서도 수학적 함수 정의의 장점을 유지할 수 있습니다.
이제 '합성(compose)'이라는 또 다른 필수 도구를 배워보겠습니다.
연습 문제
문제 풀이 관련 참고사항
Gitbook에서 읽으시면(권장) 브라우저 내에서 바로 연습문제를 풀 수 있습니다.
이 책의 모든 문제에서는 부록 A, B, C에 정의된 전역 헬퍼 함수를 사용할 수 있습니다. 게다가 특정 문제 전용으로 정의된 함수들도 활용 가능합니다. 사실상 모든 리소스를 활용해도 된다고 생각하시면 됩니다.
힌트: 임베디드 에디터에서
Ctrl + Enter를 눌러 솔루션을 제출할 수 있습니다!
로컬 환경에서 연습문제 실행하기(선택사항)
개인 에디터로 파일에서 직접 풀고 싶다면:
- 저장소 복제(
git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git) - exercises 디렉터리 이동(
cd mostly-adequate-guide/exercises) - 권장 node 버전 v10.22.1 사용 확인(
nvm install). 자세한 내용은 책의 README 참조 - npm으로 종속성 설치(
npm install) - 각 장 폴더의 exercise_ 파일 수정으로 정답 작성
- npm으로 검증 실행(ex:
npm run ch04)
유닛 테스트가 실행되어 오류 시 힌트를 제공합니다. 참고로 정답은 solution_ 파일에서 확인 가능합니다.
실전 연습 시작!
연습해 보세요!
함수를 부분 적용하여 모든 인자를 제거하도록 리팩토링하세요. const words = str => split(' ', str);
연습해 보세요!
함수들을 부분 적용하여 모든 인자를 제거하도록 리팩토링하세요. const filterQs = xs => filter(x => match(/q/i, x), xs);
다음 함수를 고려하세요:
const keepHighest = (x, y) => (x >= y ? x : y);연습해 보세요!
keepHighest 헬퍼 함수를 사용하여 max 함수가 어떤 인자도 참조하지 않도록 리팩토링하세요.
const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);