## 함수형 프로그래밍 #1 함수형 프로그래밍은 프로그래밍 방식 중 한가지로 다른 대표적인 프로그래밍 방식으로는 객체지향형 프로그래밍이 있다. C++, Java 등 한동안 주류 였던 (그리고 지금도 여전한) 언어들 대부분이 Class통해 객체지향적으로 프로그래밍을 지원 하였고 그 결과 객체지향적 프로그래밍 방식은 가장 대표적인 프로그래밍 방식이 되었다. 해당 언어들이 메인이 되어서 객체지향적 프로그래밍이 페러다임이 되었다기 보다는 객체지향적인 방식을 도입한 덕분에 해당 언어들이 주류로 떠오를 수 있게 되었다. 그렇다면 왜 객체지향적인 프로그래밍이 대세가 되었을까. 그 이유는 아무래도 가장 인간에게 친화적인 방식이라서 그럴것이다. 초창기 프로그래밍 언어는 컴퓨터의 중점에 맞춰어서 설계되었다. 제일 처음으로는 아마도 0과 1로만 이루어진 방식이었을 것이고 점차 사람에게 익숙하도록 발전되어 `언어`라는 개념이 도입되어 사람이 알아 보기 쉽도록 개발이 이루어 졌으며 점차 기능이 고도화된 이후에는 개발하는 *방식* 또한 사람에게 친숙하도록 설계되었고 그 결과 객체지향형 프로그래밍이 나올 수 있게 되었다. 그렇다면 왜 지금 다시 함수형 프로그래밍이 떠오르고 있을까 함수형 프로그래밍에 대한 장단점은 여러가지 거론되고 있으나 큰 장점 몇가지는 아래와 같다. - 코드를 적게 작성할 수 있다. - 유지보수가 편하다. - 선언적으로 프로그래밍 하여 항상 같은 결과값을 얻을 수 있다. 물론 단점도 존재하는데 가장 큰 단점은 함수형적으로 사고하지 않는다면 한눈에 알아보기 힘들다는 점이다. 물론 함수형 프로그래밍에 익숙하다면 함수형으로 작성된 코드가 좀 더 알아보기 편하고 깔끔할 것이다. 또 다른 불편한점은 함수형 프로그래밍을 하기 위해 필요한 몇가지 자료구조들이 언어 수준에서 지원되지 않으면 전부 직접 구현해야하는 불편함등이 있다. 물론 함수형에 특화된 언어에서는 논외이지만 현재 알아볼 `javascript`에서는 구현되어 있지 않는 자료구조 등이 있다. 이런 것들은 `Rambda`나 `lodash` 같은 라이브러리에서 일부 지원은 해주지만 언어 수준으로 강제하지 않으므로 불편함은 여전히 남아있다. 그러나 이러한 단점들을 모두 보완할 정도로 함수형으로 코드를 작성할 경우 재생산성과 유지보수성이 많이 올라가며 테스트코드를 작성하게 된다면 작업량이 함수형으로 작성하기 전보다 시간이 많이 단축이 된다! 함수형 프로그래밍의 중요한 개념을 네가지로 함축 하면 아래와 같다. - 선언적 프로그래밍 - 순수함수 - 참조 투명성 - 불변성 ### 1. 선언적 프로그래밍 함수형 프로그래밍은 선언적으로 작성되어야 하는데 선언적 이라 함은 `멱등성`이 있다고 할 수 있다. 어디서 어떻게 실행하던 항상 같은 결과값을 주며 외부에 영향을 받으면 안된다. 즉 내부적으로 코드를 어떻게 구현 했는지, 데이터는 어떻게 흘러가는지 밝히지 않은 채 연산/작업을 표현하는 방식을 선언적 프로그래밍이라고 한다. 선언적 프로그래밍을 적용하는 대표적인 방식이 함수형 프로그래밍이며 객체지향 프로그래밍은 명령형, 절차적으로 프로그래밍 된다. 아무래도 명령을 하거나 위에서 아래로 내려가면서 개발하는 방식이 좀 더 편할 수는 있지만, 테스트 코드를 작성하거나 어플리케이션의 복잡도가 높아질 수록 코드의 유지보수성은 떨어지며 리팩토링하기가 매우 어려워진다. 또한 항상 같은 결과값을 기대할 수 없으므로 **언제 어디서 에러가 나오는지 모르는** 위험도 항상 안고있는 것이다. 명령형 / 절차적 프로그래밍작 ``` javascript const array = [0, 1, 2, 3] for (let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2); } ``` 간단하게 배열을 정의하고 각 배열의 값을 제곱하도록 하는 코드이다. 명령형 / 절차적으로 작성하게 될 경우 **원하는 작업을 *어떻게* 하는지** 표현한다. 반면 선언적 프로그래밍은 ``` javascript [0, 1, 2, 3].map(num => Math.pow(num, 2); ``` 명령형 / 절차적 프로그래밍에서 3줄이나 하던 코드가 한줄로 줄었다. 또한 한눈에 보기 편해졌으며 결과가 쉽게 예상된다. 이와같이 선언적으로 프로그래밍을 한다는 것은 ** 프로그래밍 로직이 무엇인지를 *표현식* **으로 나타낸다. 선언적 프로그래밍은 의외로 친숙한데 가장 대표적으로 선언적으로 사용하는 것이 SQL 쿼리 이기 때문이다. ``` sql SELECT * FROM * ``` ### 2. 순수함수 순수함수는 주어진 입력에만 의존하며 평가 도중 또는 호출 간 변경될 수 있는 숨겨진 값이나 외부 상태와 무관하게 작동한다. 전역 객체나 레퍼런스로 전달된 매개변수를 수정하는 등 함수 스코프 밖에선 어떠한 변경도 일으키지 않는다. 이 말은 `Side Effect`를 일으키지 않는다는 뜻이며 순수함수 또한 선언적 프로그래밍과 마찬가지로 어디서 어떻게 호출하던 항상 같은 결과값을 내포하며 글로벌한 값들의 변경이 일어나서는 안된다. 아래는 불순 함수의 예시로 외부에 있는 변수를 건들여 호출시마다 결과값이 다르게 나온다. ``` javascript let counter = 0; function increment() { return counter++; } ``` 또는 자바스크립트에서 날짜 관련된 데이터를 핸들링하는 `Date` 또한 호출할때마다 다른 결과값을 리턴하므로 불순함수라 볼 수 있다. ``` javascript new Date() ``` 여기서 순수함수의 가장 중요한 부분은 `Side Effect(부수효과)`를 일으키지 않는다는 것이다. 부수효과의 몇가지 예로는 - 전역 범위에서 변수, 속성, 자료구조 변경 - 함수의 원래 인수 값을 변경 - 사용자 입력을 처리 - 예외를 일으킨 해당 함수가 붙잡지 않고 그대로 예외를 던짐 - 화면 또는 로그 파일에 출력 - HTML 문서, 브라우저 쿠키, DB에 질의 "아니, 브라우저 웹 화면을 갱신하고 이벤트 처리를 하는게 자바스크립트의 역활인데 HTML 문서를 건드리거나 사용자 입력을 처리하는게 부수효과이므로 피해야 한다니?! 그럼 자바스크립트를 쓰지 말라는 말인가?" 라는 생각이 들 수도 있다. 물론 함수형 프로그래밍에서 또는 프로그래밍에서 부수효과 자체가 나쁘다는것이 아니라, 부수효과를 예상치 못한 곳에서 일어날 경우 결과를 예측하기 어려우므로 잘 다뤄야 한다고 권고하고 있다. 함수형 프로그래밍에서 부수효과를 어떻게 유연하게 다루는 지는 잠시 후 보도록 하겠다. ### 3. 참조 투명성 참조 투명성은 쉽게 말해 동일한 입력을 받았을때 동일한 출력을 하는 것이다. 이것 또한 선언적, 순수함수와 같이 핵심은 같은 것을 강조하고 있다. 함수형 프로그래밍의 철학적 정의는 짧고 간결하다. 문제는 이 철학을 이해해야만 우아하고 아름답게 느껴질 것이다. 참조 투명성이 지켜지지 않을 경우는 위의 예제와 같이 외부 변수에 의존하여 값을 내는 것이다. ``` javascript let counter = 0; function increment { return counter++; } ``` 그것보다는 아래와 같이 참조 투명성을 지켜주는 것이 좀더 안전한 코드가 된다. ``` javascript const increment = counter => counter + 1; ``` 위와 같은 함수를 명령형으로 작성할 경우 ``` javascript increment(); increment(); ``` 와 같이 위에서 하나씩 명령을 날릴 것이다. 뭔가 한눈에 들어오지도 않고 결과를 예측하기도 어렵다. 좀 더 우아하게 작성하자면, ``` javascript const plus2 = run(increment, increment); ``` 이렇게도 작성할 수 있다! 우선 함수명을 `plus2`로 함으로써 본 함수를 실행 했을 경우 어떤 결과값을 리턴했는지 유추 해볼 수 있다. (물론 plus2보다 나은 함수명이 있을 수 있지만 넘어가기로 하자) `run`이라는 함수가 어떤 역활을 하는지는 아래에서 좀 더 자세히 알아보도록 하겠다. 우선 지금, `run`같은 함수가 어떻게 내부가 구성되어있는지는 모르겠지만, 함수형으로 작성되었다면 `increment`라는 함수를 실행시켜주는 역활을 하는 것 같다. 그리고 그 결과를 plus2에 저장하는 의미이지 않을까 라고 유추해 볼 수 있다. 이것이 바로 함수형 프로그래밍에서 추구하는 것이다! 내부 로직이 어떻게 되어있는지 신경 쓰지 않고 함수명을 통해 실행 결과를 유추하고 항상 같은 값을 기대할 수 있는 것이다. 또한 테스트 케이스를 작성할때 run 이라는 함수가 올바르게 동작 하는지 통과한다면, plus2를 테스트 할 필요는 없다. 항상 올바른 값이 나올테니. 여기선 매우 간단해서 아직 왜 필요한지 이렇게 어렵해 작성해야하는지 의문이 들 수도 있다. 개발 뿐만 아니라 인생 어떠한 것이든 절대적인 정답은 없으며 상황에 따라 유연하게 대처해야 함이 필요한데, 함수형이 대규모 어플리케이션에 적용되었을때의 기대효과를 생각해 보면 함수형 프로그래밍이 추구하는 가치가 얼마나 큰지를 느낄 수 있다. (여기선 1+1=2 라고 표현하는게 제일 최고의 방법일 것이다!) ### 4. 불변성 불변성이란 원시데이터와 같이 한번 생성 된 후 변경되지 않는다는 것이다. 이 불변성이라는 것이 개발을 하다보면 가장 지켜지기 힘든 부분인것 같다. 특히 자바스크립트에서 객체나 배열을 다룰때 불변성이 지켜지지 않아 데이터가 오염되기도 하고 기대했던 값이 달라지기도 한다. 이런 불변성을 지키기 위해 [immutable](https://immutable-js.github.io/immutable-js/)이나 [lodash](https://lodash.com/)등을 통해 데이터를 안전하게 복사한 후 값을 변경하고 다시 저장하는 방식을 사용하기도 한다. 함수형 프로그래밍의 핵심은 이렇게 네가지 정도로 함축할 수 있다. 사실 네가지로 함축하는 것도 많은것 같다. 위의 네가지 모두 이름은 다를 뿐 추구하는 결과는 같으니까. 역시 진리는 단 한가지인 것 같다. 위의 네가지만 유념하며 프로그래밍을 해도 함수형으로 프로그래밍 하는데 입문 할 수 있지만 함수형 프로그래밍을 하기 위해 필요한 개념들이 몇가지 더 있다. ### 1급 객체 (일급 시민) 1급 객체, 즉 일급 시민이라는 것은 말은 어렵지만 매우 간단하다 정확한 표현은 아니지만, 요약하면 모든 것들을 '값'으로 생각할 수 있다는 것이다. 자바스크립트가 아닌 다른 언어로 개발하다 자바스크립트를 접하게 되면 드는 의문중 하나가 어떻게 함수를 파라미터로 넘기지? 라는 부분이 있을 것이다. 답을 말하자면 자바스크립트에서 함수는 1급 객체이기 때문이다. (실제로 함수는 그냥 객체이다. 자바스크립트에서 모두 거슬러 올라가면 객체를 만나게 된다.) 1급 객체는 아래와 같은 역활을 한다. - 변수나 데이터 구조에 담을 수 있다. - 파라미터로 전달이 가능하다 - 반환값으로 사용이 가능하다 - 동적으로 파라미터에 할당이 가능하다 이렇게 놓고 보면 그냥 원시데이터인데? 라는 생각이 들 수도 있다. 함수가 어떻게 파라미터로 전달이 가능한지 이해하기 어렵다면 그냥 원시데이터 처럼 값이라고 생각하면 좀 더 이해하기 수월하다 string 이나 number처럼 함수 파라미터로 전달하고 리턴값으로 사용가능하다고 생각하면 전혀 이상할 게 없지 않은가! 이러한 함수들을 ** 고차함수(고계함수)** 라고 부르기도 한다. ``` javascript function applyOperation(a, b, opt) { return opt(a, b); } const multiple = (a, b) => a * b; applyOperation(2, 3, multiple); // => 6 ``` 일급시민을 먼저 언급한 이유는 일급시민이기 때문에 가능한 아래의 몇가지를 더 설명하기 위함이다. ### 클로저 ``` javascript function farm(apple, orange) { const apples = apple; const oranges = orange || 0; return { getOrange() { return oranges; }, getApple() { return apples; } }; } const farmer = farm(5, 7); farmer.getOrrange // => 7; ``` 자바스크립트의 핵심 기능들 중 하나인 클로저는 어렵다고 느낄 수 있지만 매우 간단하다. ***함수를 선언할 당시의 환경에 함수를 묶어둔 자료구조 (정적 스코프, 어휘 스코프)***를 클로저 라고 하며 이러한 개념을 모르더라도 매우 빈번히 사용되는 프로그래밍 기법 중 하나이다. 자바스크립트에서 실제 클로저가 가능한 이유는 실행 컨텍스트에 당시가 중요한데, 클로저로만 한페이지 분량으로 설명이 가능하지만 함수형 프로그래밍에 집중하기로 하고 자세한 동작원리가 궁금하다면 [여기](https://poiemaweb.com/js-execution-context)를 참고해보는 것도 좋다. ### 재귀 함수형 프로그래밍을 좀 더 어렵게 만드는 것 중 하나가 바로 재귀이다. 재귀를 이해하면 함수형 프로그래밍을 이해한것이나 다름없다 라는 말도 있듯이 재귀는 일반적으로 이해하기 어려운 방식이다. 재귀를 이해했다 하더라도 응용해서 사용할때는 고민을 한 두번씩 하게 된다. 그만큼 일반적인 사고로 정의된 것은 아니다. 재귀는 ***주어진 문제를 자기 반복적인 문제들로 잘게 분해한 후 다시 조합해가는 과정***으로 쉽게 말해 종료 시점을 정의한 후 본인을 계속 반복해 나가는 것이다. ``` javascript // 재귀가 아닐 경우 let acc = 0; const nums = [1, 2, 3, 4, 5] for (let i = 0; i < nums.length; i++) { acc += nums[i]; } // 재귀 function sum(arr) { if(_,isEmpty(arr) { // 종료 지점, 기저 케이스 return 0; } return _.first(arr) + sum(_.rest(arr)); } sum([]) // => 0 sum([1, 2, 3, 4, 5, 6, 7, 8, 9]) // => 45 ``` 재귀는 사용하는 것도한 조심스럽게 다루어야 하는데 재귀 함수의 파라미터로 함수를 넘길 경우 함수 call 로 인해 스택 오버플로우가 일어날수도 있다. 따라서 위와 같이 꼬리재귀함수를 사용하여 그러한 점을 방지할 수 있다. ### 메서드 체이닝 메서드 체이닝은 jQuery를 사용해본 사람이라면 매우 친숙한 방식이다. 매서드를 호출 한후 자신을 리턴하여 추가적으로 호출이 가능하도록 하는 방식을 메서드 체이닝이라고 한다. ``` javascript _.chain(names) .filter(isValid) .map(s => s.replace(/_/, " ")) .uniq() .map(_.startCase) .sort() .value(); ``` ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582027960940-image.png) 메서드 체이닝을 사용할 경우 함수에 단계별로 값을 계산한 후 리턴하므로 외부 값을 수정하지 않고 값을 얻을 수 있으며 가독성이 향상되었다는 장점이 있다 그러나 자신이 소유한 객체에 묶여있어 표현이 제한되어있다는 큰 단점이 존재한다. 이러한 점을 어떻게 해결하는지는 아래에서 다루도록 하겠다. ### 파이프라인 파이프 라인은 함수의 출력이 다음 함수의 입력이 되게끔 느슨하게 배열한 ***방향성 함수 순차열*** 이다. 파이프 라인에서 핵심은 함수의 *항수*와 *형식*이 호환 되어야 한다. 수신 함수는 앞 단계의 함수가 반환한 값을 처리하기 위해 한개 이상의 매개변수가 필요한데 이것을 **항수** 라고 한다. 형식이 호환된다 라는 것은 말 그대로 함수의 반환 형식과 수신 함수의 인수 형식이 일치함을 의미한다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582028877249-image.png) 파이프 라인으로 연결된 모든 함수의 형식이 일치할 필요는 없으며 수신 함수와 발신 함수의 형식이 일치하기만 하면 된다. 위의 그림과 같이 A Type의 값이 파이프 라인을 거쳐 C Type으로 나오게 되며 이러한 것들이 가능한 것은 파이프라인의 항수와 형식이 호환되어 있기 때문이다. 코드를 보면 좀 더 이해하기 편하다. ``` javascript // trim :: String -> String const trim = str => str.replace(/^\s*|\s*$/g, ''); // normalize :: String -> String const normalize = str => str.replace(/\-/g, ''); normalize(trim(' abc-fv-dc ')); // -> 'abcfvdc' ``` 지금까지 함수형 프로그래밍이란 무엇인지, 함수형 프로그래밍이 추구하는 가치와 사전에 알아야 하는 필수 개념을 간단하게 살펴보았다. 함수형 프로그래밍 2 에서는 함수형 프로그래밍에 필요한 자료구조와 추가 개념을 더 알아보면서 함수형 프로그래밍에 더 가까이 다가가보도록 하겠다. **참고** 함수형 사고, 한빛미디어 2016 함수형 자바스크립트, 한빛미디어 2018