함수형 프로그래밍은
부수 효과를 자제하고 순수함수를 추구하며
모듈화를 통한 함수 간의 조합성을 강조하는 프로그래밍 방법론을 말한다.
개인적으로 느끼기에는 순수함수를 작성하고 (JS에서는) 함수가 일급 함수임을 이용해서 함수의 인자나 결과값에 함수를 사용하는 프로그래밍 패러다임 정도인 것 같다. 특별히 FP을 위해 설계된 엘릭서와 같은 언어도 존재한다.
(어느 문서에서 읽어보니 프로그래밍 언어가 함수를 1급 함수로 취급하면[함수를 값처럼 다룰 수 있다면] 해당 언어는 함수형 프로그래밍 언어라고 부를 수 있다고 합니다. 당연히 함수형 프로그래밍이 가능하다는 이야기겠죠)
(완벽히 모든 함수를 순수함수로 만들거나 모듈화가 가능하게 하는 것은 불가능하니 강박적으로 작성하려고 하지 말자. 작성하고 나중에 리팩토링하는 게 대부분이다.)
🚀 순수함수
동일한 인자를 주면 동일한 결과를 리턴하며 (즉, 평가 시점에 따라 값이 달라지지 않는다)
외부의 상태에 영향을 미치는 부수효과가 없는 함수를 말한다.
좀 더 자세히 적자면 다음과 같은 조건이 있다.
함수 몸통에 입출력 관련 코드가 없어야 한다
함수 몸통에서 매개변수를 변경시키면 안된다.(const/readonly 형태로만 사용)
함수 몸통에서 만들어진 결과를 즉시 반환한다.
함수 몸통에 비동기적 동작이 없어야 한다.
함수 내부에 전역 변수나 정적 변수를 사용하지 않는다.
함수가 예외를 발생시키지 않는다.
함수가 콜백 함수로 구현되었거나 콜백 함수를 사용하는 코드가 없어야 한다.
주의할 점은 순수함수가 아니라고 해서 나쁜 함수인 것은 아니다.
아래 함수는 순수함수이다.
function add(a, b) {
return a + b;
}
console.log(add(3, 4));
아래 함수는 순수함수가 아니다. 함수 밖의 변수 c가 변하게 되면 함수의 결과도 바뀔 것이기 때문이다.
let c = 10;
function add2(a, b) {
return a + b + c;
}
console.log(add2(2, 3));
// 변화!
c = 2;
// 같은 함수임에도 결과가 달라짐
console.log(add2(2, 3));
다음 함수도 순수함수가 아니다. 함수 내의 외부 변수를 변화시키는 부수 효과를 낳기 때문이다.
let c = 20;
function add3(a, b) {
c = b;
return a + b;
}
console.log(add3(2, 5));
console.log(c);
외부 변수인 객체 obj1의 값을 변형 시켰으므로 이 역시 순수함수가 아니다.
let obj1 = { val: 10 };
function add4(obj, b) {
obj.val += b;
}
add4(obj1, 6);
console.log(obj1.val);
그렇다면 순수 함수는 객체의 값을 변화시킬 수 없는 것인가? 아니다. 외부 변수인 객체를 직접 건드리지 않고 새로운 객체를 리턴하면 된다.
let obj1 = { val: 10 };
// 순수함수가 아님
function notPureFunc(obj, b) {
obj.val += b;
}
// 순수함수임
function PureFunc(obj, b) {
return { val: obj.val + b };
}
a = PureFunc(obj1, 6);
console.log(a.val); // 16
console.log(obj1.val); // 그대로 10
🚀 일급함수
자바스크립트에서는 함수가 일급함수이다.(동시에 일급 객체, 일급 시민이기도 합니다) 일급 함수는 함수를 값으로 다룰 수 있다는 것이다. 함수를 변수에 담을 수도 있고 변수에 담은 함수를 값으로 다뤄서 인자로 넘길 수도 있다.
* 1급 시민 : 변수에 담을 수 있고, 함수의 인자로 전달할 수 있고, return 할 수 있다. (대개 숫자, 문자형은 1급 시민)
* 1급 객체 : 1급 시민의 조건을 만족하는 객체. JS에서 객체는 1급 시민이므로 객체를 1급 객체로 불러도 된다.
* 1급 함수 : 1급 시민의 조건을 만족하는 함수. JS에서 함수는 1급 함수이므로 함수를 1급 함수로 불러도 된다.
쉽게 말해 함수를 값처럼 다룰 수 있다는 겁니다.
f1이라는 변수에 함수를 담았습니다. 익명함수죠.
const f1 = function (a) {
return a * a;
};
console.log(f1); // [Function: f1]
f3 함수는 인자로 a를 받아 실행한 값을 리턴합니다. 즉, 함수의 인자로 함수를 넘겨줘야 한다는 것이죠. 자바스크립트에서는 함수가 일급객체이기 때문에 가능한 일입니다.
function f3(a) {
return a();
}
console.log(
f3(function () {
return 10;
})
);
// 10이 리턴됩니다.
일급 함수의 특징을 이용해 조금 복잡한 코드를 작성해보겠습니다.
아래와 같이 함수를 반환하는 함수를 고차 함수라고 부릅니다.
function add_maker(a) {
return function (b) {
return a + b;
};
}
const add10 = add_maker(10);
// add_maker(10)이 리턴하는 것은 함수입니다.
// 즉, const add10 = function (b) { return a + b; };
// 이는 add10이라는 익명함수를 정의한 것입니다.
// 이제 add10(20)을 호출하게 되면 b에 20이 들어가게 됩니다.
console.log(add10(20)); // 30
여기서 a는 10이 들어가게 되고 b에는 20이 들어가게 되어 결과값은 30이 됩니다.
왜 그런지 하나씩 살펴봅시다.
add_maker(10)이 리턴하는 것은 함수입니다.
즉, const add10 = function (b) { return a + b; };
이는 add10이라는 익명함수를 정의한 것입니다.
이제 add10(20)을 호출하게 되면 b에 20이 들어가게 됩니다.
이어서 함수가 일급 객체임을 활용해서 다음과 같이 작성해보겠습니다.
function f4(f1, f2, f3) {
return f3(f1() + f2());
}
console.log(
f4(
function () {
return 2;
},
function () {
return 1;
},
function (a) {
return a;
}
)
);
역시 f1이 반환한 값 2, f2가 반환한 값 1을 더한 값이 3이 f3의 인자로 들어가서 3을 출력하게 됩니다.
currying
JS에서 함수형 프로그래밍을 위해 종종 사용하는 currying(커링)을 알아보겠습니다. 나중에 FP를 알아보니 커링은 고차함수와 언급되는 내용이더군요.
커링은 다중인자를 받는 함수를 단일 인자 함수열로 만드는 것이다. 다른 말로 하자면 인자를 여러개 받는 함수를 분리하여, 인자를 하나씩만 받는 함수의 체인으로 만드는 것이다.
말은 이렇게 어렵지만 사실 js에서는 함수를 값처럼 사용할 수 있으므로 함수를 리턴하여 함수의 연쇄를 만듦과 동시에, 외부 함수의 스코프에 있는 변수를 사용할 수 있는 Clousure(클로저) 특성도 이용한다. 정도로 알아두면 면접에서 답변할 때나 실제 사용할 때나 크게 무리가 없습니다.
먼저 스코프가 사용되는 간단한 예시를 보겠습니다.
함수를 리턴했으므로 사용을 위해 함수 호출 후 함수를 또 호출했습니다.
이 모양을 이용하여 커링을 사용할 수 있습니다.
function outer() {
const a = "저는 바깥 스코프에서 왔읍니다 ㅎ";
return function () {
const b = "여기는 내부";
[a, b].map((el) => console.log(el));
return;
};
}
console.log(outer()());
커링 기법을 적용한 코드를 살펴보겠습니다.
function greeting(name, age) {
console.log(`${name} is ${age}`);
}
greeting("da", 25);
위 코드는 아래와 같이 커링을 통해 분할할 수 있습니다.
// currying을 통한 구현
function curGreeting(name) {
return function (age) {
console.log(`${name} is ${age}`);
};
}
const da = curGreeting("da");
da(25);
좀 더 간략하게 arrow function을 이용하면 다음과 같이 작성할 수 있을 겁니다.
// arrow
const arrowCurGreeting = (name) => (age) => {
return console.log(`${name} is ${age}`);
};
const martin = arrowCurGreeting("martin");
martin(33);
위 코드에서와 같이 커링을 사용하면, 매번 인자가 두 개인 함수를 호출하지 않고, 동일한 함수를 재사용할 수 있다는 장점이 있습니다.
그런데 커링의 실제 사용에 있어서 depth가 깊어질수록 아래와 같은 꼴로 사용될 것입니다.
curry10()()()()()()()()()()()
이러한 verbose함을 방지하고 좀 더 간결하게 사용하는 방법으로 부분 적용(Partial application) 이라는 기법이 있습니다.
이는 추후에 살펴보도록하겠습니다.
참고한 글)
https://www.zerocho.com/category/JavaScript/post/579236d08241b6f43951af18
http://jeonghwan-kim.github.io/js/2017/04/17/curry.html
https://edykim.com/ko/post/writing-a-curling-currying-function-in-javascript/
'Programming paradigm > 🧱 functional JS' 카테고리의 다른 글
Lazy Evaluation(지연 평가)와 지연성 map, filter (0) | 2021.05.24 |
---|---|
Array.prototype.[method]보다 다형성 높은 map, filter, reduce (0) | 2021.05.23 |
함수형 프로그래밍 개요 : 고차 함수, 클로저, 함수 조합 (0) | 2020.08.25 |
FP을 위한 선언형 프로그래밍(declarative) (0) | 2020.08.25 |