※ 본 카테고리의 내용은 부스트캠프 챌린지 기간동안 학습한 내용을 바탕으로 정리한 내용입니다.
목차
0. 함수형 프로그래밍
대부분의 학생들은 함수형 프로그래밍에 대해서 익숙하지 않을 것입니다.
우리가 배우는 대부분의 언어, 특히 C/C++, Java, Python가 모두 명령형 언어에 가깝기 때문입니다.
그렇다면, 지금도 명령형 프로그래밍을 잘 하고 있는데 도대체 왜 함수형 프로그래밍을 시도하려고 하는지 궁금하지 않으신가요?!
함수형 프로그래밍은 모든 내용을 순수함수로 나누어서 문제를 해결하는 기법으로 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지보수가 편리하도록 돕습니다.
여기서 순수함수란 리턴값이 외부의 영향(side-effect)을 받지 않아 항상 동일한 값을 리턴하는 함수를 말합니다.
외부의 영향을 받지 않으면 모듈화 수준이 매우매우매우 높아지기 때문에 사후관리가 굉장히 편리해집니다.
함수형 프로그래밍의 특징은 아래 4가지로 나눌 수 있습니다.
- 불변성(Immutable)
- 참조 투명성(Referential Transparency)
- 일급 함수(First-Class Function)
- 게으른 평가(Lazy Evaluation)
0.0. 불변성
불변성은 어떤 값의 상태를 변경하지 않는다는 의미를 가집니다.
상태를 변경하게 되면 그로 인해 함수에 영향이 갈 수 있겠죠?
이런 부수효과를 피하기 위해서 함수형 프로그래밍에서는 이를 최대한 제한하고 있습니다.
만약 이런 상태 변경을 막는다면 무분별하게 상태를 참조하거나 변경하는 일이 없어 프로그램이 어떻게 돌아가는지 쉽게 추적할 수 있게 됩니다.
따라서 개발자가 예상하지 못한 버그가 발생하는 것을 어느 정도 억제할 수 있겠죠.
또한 불변성은 동시에 여러 코어/스레드가 돌아가는 현대 사회에서 어떤 코어/스레드가 어떤 일을 담당하고 있는지, 어떤 일을 수행했는 지 파악하기 편하게 만들어주기도 합니다.
이 불변성은 지난 시간에 공부한 객체지향 프로그래밍과의 큰 차이이기도 합니다.
객체지향 프로그래밍은 변경이 가능하지만 사용자에게 변경 가능한 상태를 숨기는 것이라면, 함수형 프로그래밍은 아예 변경 자체를 막습니다.
또한 실제로 객체지향 프로그래밍의 경우 상태를 잘 변경하려는 의도에서 만들어진 경우가 많습니다.
관리하기 편하게 특정 객체를 만들어 놓고 그 안의 변수만을 바꿔가면서 관리하는 경우는 개발을 막 배우고 있는 우리 주변에서도 많이 볼 수 있죠?!!
이 과정은 함수형 프로그래밍에 있어서는 정말 스트레스받는 일이라는 것입니다. 우리의 필요와는 별개로요.
결국 불변성에는 장단점이 존재한다는 말과 같습니다.
더 자세한 내용을 알아보시려면 아래 불변성에 대해 잘 정리된 게시물을 참고하시면 도움이 될 것입니다 :)
변하지 않는 상태를 유지하는 방법, 불변성(Immutable)
이번 포스팅에서는 순수 함수에 이어 함수형 프로그래밍에서 중요하게 여기는 개념인 에 대한 이야기를 해보려고 한다. 사실 순수 함수를 설명하다보면 불변성에 대한 이야기가 꼭 한번은 나오
evan-moon.github.io
0.1. 참조 투명성
참조 투명성은 프로그램의 변경 없이도 어떤 표현식을 값으로 대체할 수 있다는 뜻입니다.
말이 조금 어렵죠? 함수가 외부 변수에 영향을 받지 않아야 한다고 이해하셔도 충분합니다!
아래 예시에서는 외부의 값(someName)에 의해 hello라는 함수가 영향을 받고 있습니다.
someName이 어떻게 될 지 hello 함수는 전혀 모르기 때문에 hello()는 `Hello, ${someName}.` 으로 대체될 수 없습니다.
const someName = 'Boost';
function hello() {
console.log(`Hello, ${someName}.`);
}
이 부분을 아래와 같이 매개변수로 받아서 처리한다면 외부의 영향을 받지 않고 hello(name)이라는 표현식을 `Hello, ${name}.` 으로 대체할 수 있겠죠?
이렇게 바뀐 hello를 참조 투명성을 충족하는 함수라고 부릅니다.
function hello(name) {
return `Hello, ${name}.`;
}
0.2. 일급 함수
아니 함수도 급수가 있다고? 라고 너무 당황하지는 마세요!
보통 일급이면 좋은거잖아요?
일급이기 때문에 매개변수와 리턴값으로도 들어갈 수 있고, 변수 혹은 배열 등 자료구조에도 담을 수 있습니다.
이런 특징을 가진 함수를 바로 일급 함수라고 부릅니다.
같은 논리로, 일급 시민은 대상을 함수의 매개변수로 넣을 수 있고, 리턴값으로 넣을 수 있고, 자료구조 혹은 변수에 담을 수 있는 요소를 뜻합니다.
그렇게 어렵지 않죠!!
자바스크립트에서는 무려 함수도 리턴값으로 들어갑니다! 지옥의 콜백함수..
const sayHello = function() {
return function() {
console.log("Hello!");
}
}
const myFunc = sayHello();
myFunc();
0.3. 게으른 평가
보통 일반적인 언어는 코드가 실행되는 런타임에 바로 값을 평가하지만, 함수형 언어에서는 값이 필요한 시점에 평가합니다.
만약 값을 복사한다고 생각하면, 실제로 사용에 필요한 시점까지 복사하지 않고 참조만 하고 있다가 실제로 사용을 하게 될 때 복사를 하는 시스템입니다.
이것을 게으른 평가(Lazy Evaluation)라고 부릅니다.
값이 필요한 시점까지 실행이 되지 않는다는 특징때문에 만약 시간이 오래 걸리는 작업이 대기하고 있더라도 실제로 그것을 사용하는 것이 아니라면 다른 작업은 빠르게 수행할 수 있습니다.
최근 많은 언어들은 일반적으로 적극적 평가(Eager evaluation) 방식을 사용하지만 게으른 평가 방식 역시 지원하는 경우가 많습니다.
1. 익명함수와 클로저, 그리고 커링
익명함수(Anonymous Function)는 말 그대로 이름이 없는 함수입니다.
우리가 자바스크립트로 무언가를 만들다보면 굳이 함수의 이름이 필요가 없는 경우가 많습니다. (특히 고차함수 사용할 때!!)
이렇게 이름 없이 함수의 기능만 원하는 경우 사용할 수 있는 것이 바로 익명함수입니다.
필요에 따라 람다식, 혹은 화살표 함수와 같은 방식으로도 사용할 수 있습니다.
아래 예에서는 function(n) {return n+n} 부분이 익명함수가 되겠죠!!
const test = function(n) {return n+n};
클로저(Closure)는 조금 더 큰 단위입니다.
함수도 일종의 클로저라고 부를 수 있죠.
클로저는 선언된 곳의 바깥에 있는 값을 그대로 가져와서 캡쳐하고 저장한 후에 닫습니다.
이 상태에서는 클로저를 다시 실행해도 바깥에 있는 값을 그대로 사용할 수 있겠죠?
이것이 클로저의 기본적인 컨셉입니다.
좀 더 엄밀히 말하면 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경에 접근할 수 있는 환경을 말합니다.
이 말은 클로저는 자신이 생성됐을 때의 환경을 기억하는 함수라는 말로 요약할 수 있습니다.
즉, 내부 함수가 외부에 접근해서 무언가를 하는 경우 그 구조를 클로저라고 부를 수 있는 것이죠.
아래 예시에서 closure(2)로 만든 closureTest라는 이름의 함수는 num이라는 매개변수를 받아서 num % 2 != 0 여부를 확인하게 됩니다.
따로 지정하지 않아도 앞서 넣었던 a = 2(외부 요소)는 유지되어서 매개변수 num은 매번 2로 나눈 값을 확인하게 됩니다.
const closure = (a) => {
return num => num % a != 0;
}
const closureTest = closure(2);
console.log(closureTest(16)); // false
console.log(closureTest(15)); // true
마지막으로 커링은 여러 인자를 가진 함수를 호출할 때 파라미터의 수 보다 적은 수의 파라미터를 인자로 받으면 옆에 누락된 파라미터를 추가적으로 인자로 받는 기법을 말합니다.
말로만 들으면 굉장히 복잡해보이지만, 클로저와 상당히 유사하다고 볼 수 있습니다.
아래 커링의 예가 있습니다.
curry 함수는 기본적으로 f라는 함수형 매개변수 하나만 받지만, 그 안을 보면 함수가 연쇄적으로 있어서 a와 b라는 매개변수를 추가로 받아서 f(a,b)라는 값을 반환합니다.
sum이라는 함수가 구현이 되어있다고 가정하고 curry(sum)을 함수 이름으로 사용할 요소(curriedSum)에 넣으면, 이제 우리는 그 요소를 이용해서 새로운 함수처럼 사용할 수 있게 됩니다.
클로저에서 f라는 함수를 저장해서 사용하는 것과 일치한다고 보셔도 무방합니다!
실제로 사용할 때는 함수 안에 또 다른 함수가 리턴값으로 존재하는 것이므로 괄호가 연속으로 붙어서 그 안에 매개변수를 넣어서 사용하면 됩니다.
function curry(f) { // 커링 변환을 하는 curry(f) 함수
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// usage
function sum(a, b) {
return a + b;
}
let curriedSum = curry(sum);
alert( curriedSum(1)(2) ); // 3
2. 고차함수
일급 함수, 그리고 클로저를 배웠다면 고차함수도 쉽게 이해하실 수 있습니다.
어떤 프로그래밍 언어의 함수 구현에서 함수를 인자로 넘길 수 있거나 반환할 수 있을 때 함수를 인자로 받거나 결과로 반환하는 함수를 고차함수라고 부릅니다.
대표적으로 map, filter, reduce 등이 있습니다.
간단하게 설명하였는데, 더 자세하게 알고싶으신 분들을 위해서 모질라 MDN 사이트 링크도 같이 걸어놓았으므로 참조하시면 되겠습니다 :)
2.0. map
map은 배열 내의 모든 요소 각각에 대해 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환합니다.
고차함수중에 가장 자주 쓰는 요소 중 하나입니다.
아래 예시에서는 [1,4,9,16] 이라는 배열을 받아서 그 요소들을 하나씩 x에 넣어서 2를 곱한 후에 새로운 배열에 넣어줍니다.
[1*2, 4*2, 9*2, 16*2]가 되어서 [2, 8, 18, 32]가 나오게 됩니다. 쉽고 간단하죠!
const array1 = [1, 4, 9, 16];
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1);
// expected output: Array [2, 8, 18, 32]
Array.prototype.map() - JavaScript | MDN
map() 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환합니다.
developer.mozilla.org
2.1. filter
filter는 배열의 요소들을 특정 함수에 넣어서 그 함수를 통과하는 요소들을 모아서 새로운 배열을 반환합니다.
아래 예시에서는 words 배열에 있는 요소를 순서대로 하나씩 뽑아서 word에 넣어주고, 6글자 이상인 경우에만 새로운 배열에 담아서 최종적으로 result에 걸러진 새로운 배열이 담기게 됩니다.
배열 안에서 조건에 맞는 요소만 뽑고 싶을 때 유용하게 사용할 수 있습니다.
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word.length > 6);
console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]
Array.prototype.filter() - JavaScript | MDN
filter() 메서드는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환합니다.
developer.mozilla.org
2.2. reduce
reduce는 배열의 각 요소에 함수를 실행하고, 하나의 결과값을 반환합니다.
reduce의 콜백함수의 매개변수로 맨 앞에 결과값으로 남을 변수가 들어가고, 그 뒤에는 처리할 요소를 담을 변수가 들어갑니다.
콜백함수 밖에도 initialValue를 넣을 수 있는데, 초기값을 제공하지 않으면 자동으로 배열의 맨 첫 번째 요소를 사용하게 됩니다. 만약 빈 배열에서 reduce를 쓰게 된다면 반드시 이 initialValue값이 존재해야 합니다.
아래 예시에서는 reducer이라는 화살표 함수가 하나 보이네요.
만약 이 reducer가 reduce 고차함수의 매개변수도 들어가게 된다면, 결과값 변수인 accumulator에 지속적으로 currentValue라는 array1의 요소가 들어갈 변수의 값이 계속해서 더해지게 되겠죠?
그 아래에서 실제로 진행되고 있는 모습입니다.
만약 initialValue가 5로 주어졌다면, 초기값에 5가 들어가서 5+1+2+3+4가 되는 모습입니다.
맨 아래 예시에서는 바로 위에 있는 reduce 고차함수를 풀어서 작성한 모습입니다.
reducer라는 함수가 아닌 이름이 없는 익명함수가 들어가도 똑같이 작동합니다!
reduce는 위의 두 고차함수보다 처음에 이해하기 상당히 까다롭지만, 한번 익혀두면 reduce 하나만으로 map과 filter를 모두 구현할 수 있기 때문에 매우 유용하게 사용하실 수 있습니다.
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10
// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15
console.log(array1.reduce((acc, curr) => {acc + curr}, 5);
// expected output: 15
Array.prototype.reduce() - JavaScript | MDN
reduce() 메서드는 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과값을 반환합니다.
developer.mozilla.org
2.3. forEach
forEach는 주어진 함수를 배열 요소 각각에 대해 실행합니다.
말로만 들으면
"이게 도대체 map이랑 무슨 차이가 있냐..?"
라고 생각할 수 있는데
map은 리턴값을 받을 수 있지만 forEach는 리턴값을 받을 수 없습니다.
실제로 특정 변수에 값 혹은 배열을 리턴받고 싶다고 return을 넣어도 반환되지 않습니다.
또한 forEach는 기존의 Array를 변경하고, map은 새로운 Array를 반환합니다.
각 차이에 따라 본인이 유용하게 사용할 수 있는 함수를 사용하시면 되겠습니다.
let arr = [1,2,3,4,5];
let a = arr.forEach(function(value){
return value;
});
console.log(a); //undefined : 반환 X!!!
Array.prototype.forEach() - JavaScript | MDN
forEach() 메서드는 주어진 함수를 배열 요소 각각에 대해 실행합니다.
developer.mozilla.org
최근댓글