※ 지난번 4장 리뷰를 이어가도록 하겠습니다.
Chapter4. 재사용 가능한, 모듈적인 코드로
4.5 함수 파이프라인을 합성
함수형 프로그램의 목표는, 함수형 사상의 중추인 합성을 유도하는데 필요한 자료구조를 얻는 것입니다. 합성을 하기위해선, 반드시 함수에서 부수효과를 없애야 합니다.
4.5.1 HTML 위젯에서 합성하기
중요한 내용은 없으니 생략하겠습니다.
4.5.2 함수 합성 : 서술과 평가를 구분
함수 합성이란 한마디로 복잡한 작업을 한데 묶어 간단한 작업으로 쪼개는 과정입니다. 먼저 람다JS 의 R.compose 로 두 순수함수를 합성하는 예시를 보겠습니다.
const str = `We can only see a short distance
ahead but we can see plenty there
that needs to be done`;
const explode = str => str.split(/\s+/);
const count = arr => arr.length;
const countWords = R.compose(count, explode);
countWords(str); // -> 19
이 코드에서 흥미로운 사실은, countWords 함수를 실행하기 전에는 아무 평가도 하지 않는다는 점입니다. 함수의 서술부와 평가부를 분리하는 함수 합성의 미학이라고 할 수 있습니다.
함수의 합성은 자바스크립트에서 기본 지원 대상이 아니지만 위의 예시처럼 R.compose 를 사용해 고계함수로 자연스럽게 표현 가능하며, 관련한 예시를 하나 더 보겠습니다.
const trim = str => str.replace(/^\s*|\s*$/g, ''); // 문자열 앞 뒤 공백 제거
const normalize = str => str.replace(/\-/g, ''); // 대시를 모두 지움
const validLength = (param, str) => str.length === param; // 문자열 길이 체크
const checkLengthSsn = _.partial(validLength, 9); // SSN 길이가 9인지 체크
const cleanInput = R.compose(normalize, trim); // normalize 와 trim 합성
const isValidSsn = R.compose(checkLengthSsn, cleanInput); // cleanInput 과 checkLengthSsn 합성
cleanInput(' 444-44-4444 '); // -> '444444444'
isValidSsn(' 444-44-4444 '); // -> true
이는 함수에만 국한된 개념이 아니며, 전체 프로그램 역시 부수효과 없이 다른 프로그램/모듈을 합성해서 만들 수 있습니다. 합성은 결합 가능한 연산이라서 논리 AND 연산자로 원소를 합칠 수 있으며, 프로그램 또한 이런 식으로 부속품을 조립해서 만드는 것입니다.
R.compose 를 자바스크립트의 Function 프로토타입에 추가해 기능을 확장할 수도 있습니다.
Function.prototype.compose = R.compose;
const cleanInput = checkLengthSsn.compose(normalize).compose(trim);
4.5.3 함수형 라이브러리로 합성
람다JS 의 기능을 좀 더 사용해 합성을 해보도록 하겠습니다. 아래 예시)
// 학생과 점수 리스트
const students = ['Rosser', 'Turing', 'Kleene', 'Church'];
const grades = [80, 100, 90, 99];
// 가장 똑똑한 학생 구하기
const smartestStudent = R.compose(
R.head, // 첫번째 원소를 얻음
R.pluck(0), // 주어진 인덱스에 위치한 원소를 추출해 새 배열로 만듬
R.reserve, // 전체 배열을 거꾸로 뒤집음
R.sortBy(R.prop(1)), // 주어진 속성을 기본 오름 차순으로 정렬
R.zip // 인접한 배열 원소끼리 짝지어 새로운 배열 반환 -> [['Rosser', 80], ['Turing', 100], ...]
);
smartestStudent(students, grades); // -> 'Turing'
위의 합성에 사용된 함수들을, 알기 쉽게 별칭을 사용할 수도 있습니다.
const first = R.head;
const getName = R.pluck(0);
const reverse = R.reverse;
const sortByGrade = R.sortBy(R.prop(1));
const combine = R.zip;
R.compose(first, getName, reverse, sortByGrade, combine);
이런식의 활용은 가독성면에선 좋아지지만, 특정한 경우에만 쓸 수 있는 함수들이라서 재사용 면에선 별 장점이 없습니다.
위의 예시들처럼 전체 코드를 순수함수로 표현했지만, 실무에선 늘 이렇게 하기란 불가능합니다. 외부 저장소에서 데이터를 읽거나 원격 http 를 요청하는 등의 부수효과를 피할 수 없기 때문입니다. 따라서 순수 코드와 불순 코드를 떼어놓고 분별할 수 있어야 합니다.
4.5.4 순수/불순 함수 다루기
불순한 코드는 실행 후 부수효과가 드러나고 외부 디펜던시 탓에 구성 함수의 스코프 바깥에서 데이터에 접근할 수밖에 없습니다. 외부 데이터를 참조 및 직접 수정하는 부수효과를 일으키는 코드를 함수형으로 리팩토링 하는 부분을 아래 예시를 통해 보도록 하겠습니다.
// findObject :: DB -> String -> Object
const findObject = R.curry((db, id) => { /* 리팩토링한 find() 메서드는 저장소 객체를 매개변수로 받기에 합성하기 쉬움 */
const obj = find(db, id);
if (!obj) throw new Error(`ID 가 [${id}]인 객체는 없습니다`);
return obj;
});
// findStudent :: String -> Student
const findStudent = findObject(DB('students')); /* students 객체 저장소를 가리키게 하여 findObject 부분평가 하는 새 함수 생성 */
const csv = ({ ssn, firstname, lastname }) => `${ssn}, ${firstname}, ${lastname}`;
// append :: String -> String -> String
const append = R.curry((elementId, info) => {
document.querySelector(elementId).innerHTML = info;
return info;
});
// showStudent :: String -> Integer
const showStudent = R.compose( /* 합성을 통해 하나의 실행 가능한 단위로 엮음 */
append('#student-info'),
csv,
findStudent,
normalize,
trim
);
showStudent('44444-4444'); // -> 444-44-4444, Alonzo, Church
위의 예시에선 trim -> append 순서로 입력을 전달하는데, 이런 합성 순서가 부자연스러워 보인다면 람다JS 의 compose 대신 pipe 함수를 사용할 수도 있습니다.
R.pipe(
trim,
normalize,
findStudent,
csv,
append('#student-info')
);
R.pipe / R.compose 로 새 함수를 만들 때엔, 위 예시들 처럼 정규 인수를 명시적으로 선언할 필요 없이 새 함수를 만들어 사용 가능합니다. 함수 합성에서 권장하는 이런 스타일을 무인수 코딩이라고 합니다.
4.5.5 무인수 프로그래밍
이러한 무인수 코딩은 구성 함수의 매개변수를 하나도 드러내지 않습니다. 이러한 스타일은 또 다른 말로 암묵적 프로그래밍 이라고도 합니다. 예시를 하나 더 보겠습니다.
const runProgram = R.pipe(
R.map(R.toLower),
R.uniq,
R.sortBy(R.identity)
);
runProgram(['Functional', 'Programming', 'Curry', 'Memoization', 'Partial', 'Curry', 'Programming']);
// -> [curry, functional, memoization, partial, programming];
이러한 스타일은 간결하다는 장점은 있지만, 다소 합성을 과용한 형태이기도 하며 오히려 파악하기 어려운 모호하고 헷갈리는 프로그램이 될 수 있습니다. 또한 무인수 코드는 에러 처리, 디버깅을 할 때도 문제가 될 수 있습니다.
4.6 함수 조합기로 제어 흐름을 관리
명령형 코드는 if-else, for 같은 절차적 제어 장치로 프로그램의 흐름을 통제하지만, 함수형 코드에선 함수 조합기라는 것을 사용합니다. 조합기란, 함수 또는 다른 조합기 같은 기본 장치를 조합하여 제어 로직처럼 작동시킬 수 있는 고계함수 입니다. 위에서 본 compose 나 pipe 도 조합기이며, 자신의 변수를 선언하거나 비즈니스 로직을 두지 않습니다.
조합기의 종류
- 항등 (identity)
- 탭 (tap)
- 선택 (alternation)
- 순차열 (sequence)
- 포크 (fork) 또는 조인 (join)
4.6.1 항등 (I-조합기)
identity 조합기는 주어진 인수와 똑같은 값을 반환하는 함수입니다.
identity :: a -> a
4.6.2 탭 (K-조합기)
tap 조합기는 코드 추가 없이 공형 함수 (로깅, 파일, 페이지 쓰기 등) 를 연결하여 합성할 때 유용합니다.
이 함수는 입력 객체 a 와 함수 하나를 받아 a 에 이 함수를 실행하고 다시 a 를 반환합니다.
tap :: (a -> *) -> a -> a
const debug = R.tap(debugLog);
const cleanInput = R.compose(normalize, debug, trim);
const isValidSsn = R.compose(debug, checkLengthSsn, debug, cleanInput);
R.tap 으로 감싼 debug 를 아무리 호출해도 프로그램의 결과는 달라지지 않습니다. 함수를 실행하고 입력 객체를 그대로 반환하기 때문에, 위처럼 계산 결과도 출력하면서 디버깅도 병행할 수 있습니다.
4.6.3 선택 (OR-조합기)
alt 조합기는 함수 호출 시 기본 응답을 제공하는 단순 조건 로직을 수행합니다. 함수 2개를 인수로 받아 (false, null, undefined 가 아닌) 값이 있으면 첫 번째 함수의 결과를, 아니면 두 번째 함수의 결과를 반환합니다.
const showStudent = R.compose(
append('#student-info'),
csv,
alt(findStudent, createNewStudent) // <- findStudent 의 값이 있으면 그 값이 인자로 넘어가고, 없으면 createNewStudent 한 결과가 넘어감
);
4.6.4 순차열 (S-조합기)
seq 조합기는 함수 순차열을 순회합니다. 2개 또는 더 많은 함수를 인수로 받아, 동일한 값에 대해 각 함수를 차례로 실행하는 또 다른 함수를 반환합니다.
const showStudent = R.compose(
seq(
append('#student-info'),
consoleLog
), /* seq 조합기를 사용해 append 와 consoleLog 를 순차 실행할 뿐 값을 반환하지는 않는다 */
csv,
findStudent
);
4.6.5 포크(조인) 조합기
fork 조합기는 하나의 자원을 두 가지 방법으로 처리 후 그 결과를 다시 조합합니다. 하나의 join 함수와 주어진 입력을 처리할 종단 함수 2개를 받습니다. 분기된 각 함수의 결과는 제일 마지막에 인수 2개를 받는 join 함수에 전달됩니다.
const computeAverageGrade =
R.compose(getLetterGrade, fork(R.divide, R.sum, R.length));
/* fork 의 인자 중 R.sum, R.length 를 입력에 따라 처리한 후 R.divide 로 합침 */
computeAverageGrade([99, 80, 89]); // -> 'B'
4.7 Summary
- 함수형 체인과 파이프라인은 재사용 가능한, 모듈적인 프로그램 조각들을 연결
- 람다JS 는 커링과 합성이 주특기인 함수형 라이브러리이며, 많은 유틸리티 함수들이 존재
- 커링, 부분 적용을 통해 함수 인수를 일부만 평가하거나 단항 함수로 변환하여 함수 항수를 낮출 수 있음
- 작업을 단순한 함수들로 쪼갠 후 다시 조합하는 방법 가능
- 함수 조합기를 쓰면 실수에서 복잡한 프로그램의 흐름을 조화롭게 편성하고 무인스 스타일로 개발 가능
출처 : 함수형 자바스크립트(책)
'책 리뷰 > 함수형 자바스크립트' 카테고리의 다른 글
함수형 자바스크립트 [6장] (0) | 2020.06.30 |
---|---|
함수형 자바스크립트 [5장] (0) | 2020.06.27 |
함수형 자바스크립트 [4장 - (1)] (1) | 2020.06.02 |
함수형 자바스크립트 [3장] (0) | 2020.05.25 |
함수형 자바스크립트 [2장] (0) | 2020.05.23 |