## 함수형 프로그래밍 #2 지난 번 글에서 함수형 프로그래밍이란 무엇인지, 함수형 프로그래밍에서 추구하는 가치와 필요한 개념 몇가지를 간단하게 알아보았다. 이제 조금 더 깊게 들어가 함수형 프로그래밍에서 필요한 자료구조와 추가 개념 몇가지를 더 알아보도록 하겠다. ### 튜플 (Tuple) 튜플은 불변한 순서가 있는 객체의 집합으로서 형식이 다른 원소를 한데 묶어 다른 함수에 건네주는 불변성 자료구조이다. 함수형에서는 모든 데이터들이 불변해야 하므로 원시데이터를 제외한 객체나 배열같은 데이터 구조를 사용하기 보다는 이렇게 불변성이 유지될 수 있는 자료구조를 사용하기를 권장한다. 스칼라 같은 언어에서는 언어 차원에서 제공하는 자료구조이지만 아쉽게도 자바스크립트에서는 사용하길 원할 경우 직접 구현하거나 라이브러리를 통해 사용되어야 한다. ``` scala // 스칼라 var t = (30, 60, 90); var sumAnglesTriangle = t._1 + t._2 + t._3; //180 ``` 자바스크립트에서 튜플을 지원하지 않는 점은 아쉽지만 직접 구현해 흉내를 내보면서 튜플을 좀 더 이해해보도록 하자. 튜플을 간단히 구현하면 아래와 같다. ``` javascript const Tuple = function(/* 형식 */) { const typeInfo = Array.prototype.slice.call(arguments); const _T = function(/* 값 */) { const values = Array.prototype.slice.call(arguments); if(values.some( val => val === null || val === undefined )) { throw new ReferenceError("튜플은 null을 가질 수 없음") } if(values.length !== typeInfo.length) { throw new TypeError("튜플 항수가 프로토타입과 맞지 않음"); } values.forEach((val, index) => { this['_' + (index + 1)] = checkType(typeInfo[index])(val); }, this); Object.freeze(this); // 튜플을 불변 인스턴스화 시킨다. }; _T.prototype.values = () => Object.keys(this).map(k => this[k], this); return _T; } ``` 똑같진 않지만 위의 스칼라와 유사하게 동작하는 Tuple 자료구조이다. 여기서 핵심적인 부분은 `Object.freeze(this)`부분으로 객체를 얼려버려 더이상 수정을 할 수 없게끔 만든 후 읽기만 수행하도록 하는 것이다. 실제 사용을 한다면 아래와 같다. ``` javascript // trim :: String -> String const trim = str => str.replace(/^\s*|\s*$/g, ''); // normalize :: String -> String const normalize = str => str.replace(/\-/g, ''); const Status = Tuple(Boolean, String); // boolean 타입과 string 타입으로 튜플 선언 // isValid :: String -> Status const isValid = function(str) { if(str.length === 0) { return new Status(false, "잘못된 입력입니다."); } else { return new Status(true, "입력 성공"); } } const result = isValid(normalize(trim(' abc-aa-bbb '))); const [bool, msg] = result.values(); ``` 위에서 튜플을 선언하고 실제로 적용하였으며 파이프라인으로 함수를 호출하였다. isValid가 Status 튜플을 리턴함으로써 result의 values를 호출 하게 될 경우 기 선언된 Boolean, String 타입으로 리턴하게 된다. 최근에는 Typescript의 등장으로 타입 지정과 읽기전용으로 데이터를 사용하는데 좀 더 수월해 졌다. ### 커링(Currying) 커링이란 매우 재미있는 프로그래밍 기법으로 여러개의 인자를 가진 함수를 호출할 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면 누락된 파라미터를 인자로 받는 함수를 리턴하는 기능이다. 커링을 사용하게 될 경우 다인수 함수를 단항 함수로 바꾸어 사용할 수 있게 되므로 코드의 복잡도를 낮추게 된다. 쉽게 말해 5개의 인자를 받는 함수를 한개의 인자씩 전달하도록 구현할 수 있게 된다. 5개의 파라미터를 동시에 던지는 것 보단 한개씩 던질 경우 가독성이 훨씬 올라가게 된다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582030225156-image.png) ``` javascript const StringPair = Tuple(String, String); function curry(fn) { return function(firstArg) { return function(secondArg) { return fn(firstArg, secondArg); } } } const name = curry((first, last) => new StringPair(last, first)); const [first, last] = name("Curry")("Haskell").values(); ``` 위는 커리 내부를 간단히 구현해 본것으로 이런 식이라는 개념만 참고하면 된다 (물론 내부는 저렇게 단순하지 않다!) 커리를 사용하면 공통적으로 전달하는 파라미터들을 전달하여 선언한 후 별도의 값들을 전달하여 사용할 수 있다. 참고로 커링은 카레에서 유래된 것이 아니라 수학자 Haskell Curry의 이름에서 유래되었다. ### 부분 적용 (partial application) 부분적용은 커링과 비슷한 기법인데, 한줄로 요약하자면 ***매개변수 값을 처음부터 고정시켜 항수가 더 작은 함수를 생성하는*** 기법이다. 두가지 같은 말 아니야? 라고 할 수 있지만 커링이랑은 약간은 다른 식으로 동작한다. 커링은 부분 호출할 때마다 단항 함수를 중첩해서 생성 함으로써 내부적으로 단계별로 합성하여 최종 결과를 낸다. 여러 인수를 부분 평가하는 식으로 변용할 수 있어 평가 시점과 방법을 컨트롤 할 수 있다. 반면 부분적용은 함수 인수를 미리 정의된 값으로 묶은(할당한) 후, 인수가 적은 함수를 새로 만든다. 이 결과 함수는 자신의 클로저에 고정된 매개변수를 갖고 있으며 후속 호출 시 이미 ***평가를 마친 상태***이다. 코드로 보면 둘의 차이가 확실히 들어난다. ``` javascript // Currying const curry = function(a) { return function(b) { return function(c) { return `${a}, ${b}, ${c}`; } } } // Partial application const partial = function(a) { return function(b, c) { return `${a}, ${b}, ${c}`; } } ``` 부분 적용은 이미 `lodash`같은 라이브러리에서 휼룡하게 구현이 되어있다. 여기서 내부 구조를 일부 흉내내면 ``` javascript function partial() { const fn = this; const boundArgs = Array.prototype.slice.call(arguments); const placeholder = undefined; const bound = function() { let position = 0; const length = boundArgs.length; const args = Array(length); for(let i = 0; i < length; i++) { args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; } while(position < arguments.length) { args.push(arguments[position++]); } return fn.apply(this, args) } return bound; } ``` 매우 간단하게 구현된 partial 함수이다. 내부로 받은 인자값이 placeholder인 `undefined` 일 경우 다시 인자를 받는 식으로 동작한다. 실제 `lodash`에 `partial`을 적용하면 아래와 같다. ``` javascript String.prototype.first = _.partial(String.prototype.substring, 0, _); "Functional Programming".first(3); // -> 'Func' ``` 또는 이렇게 공통적으로 들어가는 `setTimeout`은 미리 정의를 하고 다르게 설정하는 부분만 미리 정의하여 스케쥴러 메소드들을 만들 수도 있다. ``` javascript const Scheduler = (function () { const delayFn = _.bind(setTimeout, undefined, _, _); return { delay5: _.partial(delayFn, _, 5000), delay10: _.partial(delayFn, _, 10000), delay: _.partial(delayFn, _, _) }; }) Scheduler.delay5(() => console.log("5초 후에 실행됩니다")); ``` `lodash` 에서 placeholder 는 lodash 본인인 `_` 이다. 해당 값을 넘기게 되면 다시 파라미터로 전달받기로 인식하게 된다. ### 함수합성 이전 시간에 메서드 체인을 사용할 경우 가독성이 높아지는 효과가 있지만 본인이 가지고 있는 메서드 내에서만 사용이 가능한 제약사항이 있다고 했다. 가독성과 제약사항을 뛰어넘는, 두가지 토끼를 잡기 위해서 함수 합성이라는 기법을 사용할 수 있다. 함수 합성은 말 그대로 함수들을 합성해주는 것이다. 보통 어떠한 이벤트를 처리하거나 함수를 호출해야할때 한가지 함수만 사용하는 것이 아니라 여러가지 복합적인 함수를 같이 섞어 쓰게 된다.(만약 복잡한 작업임에도 한가지 함수안에 모두 정의되어 사용된다면 잘못된 사용이다! 테스트 코드도 덩달아 복잡해지며 디버깅하기 매우 어렵게 만드는 요소이다.) 이러한 ***복잡한 작업들을 묶어 간단한 쪼개는 과정***을 함수 합성이라 하며 서술과 평가를 구분 해야한다. ``` javascript function compose(/* function */) { const args = arguments; const start = args.length - 1; return function() { let i = start; let result = args[start].apply(this, arguments); while(i--) { result = args[i].call(this, result); } return result; } } ``` 함수 합성을 간단히 구현하면 위와 같다. 함수들을 1개 이상 받아 여기선 파라미터의 마지막 함수부터 거꾸로 거슬러 올라가며 실행을 해주고 있으며 결과값을 다음 함수의 파라미터로 전달하고 있다. 이렇게 되면 함수의 흐름을 한눈에 볼 수 있어 해당 함수의 역활만 알고 있다면 결과값을 예상할 수 있을 것이다. 실제 `Rambda` 라이브러리의 `compose` 함수를 활용한 예제는 아래와 같다. ``` javascript // R 은 rambda 함수에서 import 되었다. const str = `엄청 긴 문자열 입니다. 하지만 여기서 단어들을 추출해야 합니다. 단어는 총 몇개가 있을까요`; const explode = str => str.split(/\s+/); const count = arr => arr.length; const countWords = R.compose(count, explode); countWords(str); // -> 13 ``` `exlode`와 `count`함수를 보면 각각 공백을 기준으로 잘라서 배열로 만들어 주고 파라미터로 받은 배열의 길이를 반환해주는 함수이다. 개별로 보자면 큰 역활이 없는 함수를 `compose`로 묶어서 순서대로 처리하게 만들면 문장안에 단어가 몇개인지 리턴해주는 실용적인 함수가 만들어진다. 함수형 프로그래밍은 이런 식으로 1가지 기능을 하는 함수들을 만들어 조합하여 복잡한 함수로서의 기능을 하게끔 만든다. 복잡한 듯 하지만 개별적으로 보자면 전혀 복잡한것들이 없는 함수들이다. 외부 변수를 건들이지도 않았고, 원본값을 건들여서 불변성을 깨지도 않았으며 불필요한 행동을 최소화 하여 간결하고 훌룡하게 작동하는 함수가 완성이 되었다! 위에서 사용되었던 함수 몇가지를 더 활용한 예제를 한번 보도록 하겠다. 각각 알파벳이 아닌것들을 제거 해주고 원하는 길이에 부합 하는지 확인하는 함수로 합성된 함수를 활용하여 한번 더 사용하는 방식으로도 사용될 수 있다. 중요한 것은 전달해주는 함수의 리턴값과 받는 함수의 파라미터의 타입과 개수가 동일하여야 한다. ``` javascript // trim :: String -> String const trim = str => str.replace(/^\s*|\s*$/g, ''); // normalize :: String -> String const normalize = str => str.replace(/\-/g, ''); const validLength = (param, str) => str.length === param; const checkLength = _.partial(validLength, 9); // 함수 합성 const cleanInput = R.compose(normalize, trim); const isValid = R.compose(checkLength, cleanInput); cleanInput(" abc-aa-ccc "); // -> 'abcaaccc' isValid(" abc-aa-cccc "); // -> true ``` 이번에는 좀 더 많은 함수들을 합성한 예제를 보겠다. 학생과 성적의 배열을 주고 가장 성적이 좋은 학생을 찾아내는 예제로 해당 함수의 조건은 성적의 인덱스와 학생의 인덱스가 동일해야 한다는 점 외에는 없다. 여기서 마찬가지로 `rambda` 라이브러리를 사용했으며 각 함수명을 보고 내부적으로 어떤 역활을 하는지 유추해보는 것도 좋다. ``` javascript const students = ["A", "B", "C", "D"]; const grades = [80, 90, 100, 99]; const first = R.head // 첫번째 원소를 얻는다. const getName = R.pluck(0); // 주어진 인덱스에 위치한 원소를 추출해 새 배열을 만든다. const reverse = R.reverse; const sortByGrade = R.sortBy(R.prop(1)); const combine = R.zip; // 인접한 배열 원소끼리 묶어 새로운 배열을 만든다. const bestStudent = R.compose(first, getName, reverse, sortByGrade, combind); bestStudent(students, grades); // -> "C" ``` 함수 합성을 활용하면 더이상 복잡하고 거대한 하나의 함수 보다는 잘게 조개진 함수들이 각자의 역활을 수행하여 거대한 하나의 함수가 되는 과정을 볼 수 있다. 물론 앞에서 언급한 기능들을 더해서 활용하면 좀 더 우아하게 만들 수도 있다. DB에서 학생 데이터를 받아와 특정 학생을 표시하는 함수를 만들어 보면 아래와 같다. (여기서 DB 함수는 DB에서 값을 조회하는 함수라 가정한다.) ``` javascript // findObject :: DB -> String -> Object const findObject = R.curry((db, id) => { const obj = find(db, id); if(obj === null) { throw new Error(`ID가 ${id}인 학생이 없습니다`); } return obj; }) // findStudent :: String -> Student const findStudent = findObject(DB("student")); const makeName = ({firstname, lastname}) => `${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"), makeName, findStudent ) showStudent("student-id"); ``` 여기서 잘못된 학생을 조회하려고 하면 예외를 던지도록 되어있다. 그러나 함수형 프로그래밍에서는 예외를 던지는 상황을 최대한 지양하도록 하고 있는데 1. 에러를 던질 경우 합성이나 체이닝을 할 수 없다. 2. 함수 호출 스택에서 빠져나가므로 예측 가능한 값을 지향하는 참조 투명성 원리에 위배된다. 3. 예기치 않게 스택이 풀리면 부수 효과가 일어날 수 있다. 4. 에러를 조치하는 코드가 함수를 호출한 지점과 떨어져 비지역성 원리에 위배된다. 5. 단일 반환값에 초점이 맞춰저야 하나 catch 블록에 있는 부분을 처리하므로 호출자의 부담을 가중시킨다. 6. 중첩된 에러 처리 조건은 복잡성을 가중시킨다. 위와 같은 이유로 에러를 던지는 상황은 지양한다. 한마디로 기능에 초점을 맞춰야하는 함수에 너무 많은 코드를 배치시키고 자칫 잘못하면 코드가 지저분해지고 '에러를 던질 수도 있다' 라는 전제조건으로 에러를 제대로 처리하지 못하면 예기치 못한 상황이 일어날 수도 있다는 것이다. 프로그래밍은 완벽한 컴퓨터 위에서 돌어가지만 그러한 코드를 작성하는건 사람이므로 신이 아닌이상 에러가 날 수도 있는데 에러를 던지지말라니! 라고 황당하게 들릴 수 있지만 함수형 프로그래밍에서 어떻게 에러를 우아하게 다루는지 이제 알아보도록 하겠다. ### null error 프로그램에서 일어나는 에러중 가장 빈번한 에러가 이 null error이다. null 처리를 제대로 하지 않는다면 기대값으로 작동하던 모든 함수들이 중단하고 에러를 내뿜게 된다. 그렇다 보니 아래처럼 모든 함수마다 null check를 하게되는 상황이 발생했다. ``` javascript function getComponeyInfo(employee) { let componey = employee.getComponey(); if(componey && componey !== null) { const addr = componey.getAddress(); if(addr && addr !== null) { const country = addr.getCountry(); return country; } } throw new Error("There is no componet!"); } ``` 매 함수마다 null check하는 코드가 들어가 함수의 복잡성이 높아졌으며 step도 깊어지고 있다. 이렇듯 에러 처리는 필요하지만 에러 처리를 할수록 코드가 복잡해지는 아이러니한 상황이 벌어졌다. 에러 처리를 복잡하지 않게 다루는 가장 좋은 방법은 아마도 에러가 나지 않도록 확신하는 코드일 것이다. ### 함수자 함수자는 try-catch 블록에서 예외가 날 수 있는 부분을 try로 감싼 것처럼 컨테이너로 감싸 try-catch 블록을 제거 할 수 있다. 값을 컨테이너화 하여 불변성이 지켜지도록 접근 차단 후 연산을 컨테이너에 매핑하여 값에 접근한다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582114158848-image.png) ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582114254929-image.png) 위의 그림과 같이 Wrapper에 값을 감싼 후 조회할 경우 안전한 값을 리턴하는 방식이다. 이를 코드로 옮겨보면 아래와 같다. ``` javascript class Wrapper { constructor(value) { this._value = value; } // map :: (A -> B) -> A -> B map(f) { return f(this._value); // 주어진 함수를 매핑 } } const wrap = val => new Wrapper(val); const identity = val => val; const wrappedValue = wrap("Get Functional"); wrappedValue.map(identity); // -> 'Get Functional'; ``` 값을 Wrapper 안으로 승급한 다음 수정하고 다시 Wrapper에 넣을 목적을 염두해 둔 함수 매핑이 가능한 자료구조로서 값의 형식을 보존하여 안전한 값을 받을 수 있다. ``` javascript // fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B] fmap(f) { return new Wrapper(f(this._value)); } const plus = R.curry((a, b) => a + b); const plus3 = plus(3); const two = new Wrapper(2); const five = two.map(plus3); // -> new Wrapper(5) five.map(identity); // -> 5 two.fmal(plus3).fmap(plus10); // -> new Wrapper(15) ``` 함수자는 `fmap` 처럼 사용된다. ``` javascript // 함수자 예시 // map :: (A -> B) -> Array(A) -> Array(B) // filter :: (A -> Boolean) -> Array(A) -> Array(A) // compose :: (B -> C) -> (A -> B) -> (A -> C) ``` 위에 더하기를 구현한 함수자를 좀 더 자세히 보면 아래와 같이 값을 구해오고 다시 함수자로 집어넣는 방식으로 구현 되었다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582114836435-image.png) 함수자는 사용을 하기 위해 전제조건이 몇가지 필요한데, 첫째로 부수효과가 없어야 한다. 함수를 매핑하면 항상 동일한 값을 얻어야만 한다. 둘째로 합성이 가능해야한다. 합성 함수를 이용하거나 체이닝을 이용하거나 동일한 결과값을 얻어야 한다. 물론 두가지 다 함수자여서가 아니라 함수형 프로그래밍에서의 필수 전제조건일 것이다. 얼핏 보면 데이터를 완벽히 보호하고 안전하게 느껴지지만 함수자로는 완벽하지 않다. 예외를 던지거나, 원소를 바꾸거나, 함수 로직을 변경하는 작업은 함수자로는 할 수 없기 때문에 추가로 필요한 몇가지 기능들이 있다. 함수자는 단지 ***원본값을 바꾸지 않은 상태로 연산을 수행하는것이 목표*** 이기 때문이다. 무엇보다 코드에서 나는 에러중 가장 빈번하다는 null 데이터를 다룰수 없으며 함수자가 값을 얻어오는 컨테이너에서 null 처리를 해 주어야 한다. 에러 처리를 우아하게 할 수 있다 했지만 뭔가 반쪽짜리인 기분이다. 나머지 반쪽을 채우기 위한 추가 개념들은 다음 시간에 더 알아보도록 하겠다. **참고** 함수형 사고, 한빛미디어 2016 함수형 자바스크립트, 한빛미디어 2018