함수형 자바스크립트 기본

함수형 패러다임을 자바스크립트에 적용할 때에 알면 좋은 Javascript의 기본 문법과 배경 지식을 설명한 글이다.

자바스크립트와 타 언어들간의 함수형 프로그래밍 지원의 차이

Javascript에는 없는 함수형 언어의 특징

  1. 순수 함수 강제 (side effect를 발생시키는 표현식을 허용하지 않음)
  2. 불변성 강제 (변수라고 부르지만, 상수. 객체까지도.)
  3. 재귀 강제 (반복문 미지원)

아무래도 Javascript는 멀티 패러다임이라 함수형 패러다임만 지원하는 언어에 비해서는 제약이 덜할 수 밖에 없을 것이다.

Javascript에서 이를 극복하는 방법

  1. eslint 규칙 사용으로 부분적으로 극복.
  2. ImmerJS 등의 라이브러리 도입
  3. 함수형 프로그래밍에 익숙해진다면, 괜찮을 것. (1과 마찬가지로 eslint 규칙 도입 등.)

Javascript 런타임들이 대부분 꼬리 재귀 호출 최적화를 지원하지 않기 때문에, 재귀가 타 함수형 언어에 비해 성능이 낮을 수 밖에 없다.

꼬리 재귀 호출 최적화는 재귀 호출이 함수의 마지막에서 발생하는 경우에 적용된다. 컴파일러가 자동으로 재귀를 반복문으로 치환한다. 덕분에 스택 프레임을 1개만 사용한다.

Javascript의 장점

  1. 다른 모든 함수형 언어는 학습 곡선이 높다. 누구나 이해하고 사용할 수 있다고 하기 힘들다.
  2. ES6+부터 함수형 지원이 좋은 편이다.
  3. 타 함수형 언어들에 비해 시장이 크고, Production-level Application 구축이 용이하다.

함수 실행(call, apply)과 인자(arguments), 점(.) 다시 보기

Javascript의 함수 안에서는 arguments 객체와 this 키워드를 사용할 수 있다.

※ 화살표 함수에서는 arguments를 사용할 수 없다.

1
2
3
Uncaught ReferenceError: arguments is not defined
at hi (<anonymous>:1:37)
at <anonymous>:1:1

arguments 객체

배열과 유사한 Arguments 객체(Arguments(4) [ ‘a’, ‘b’, ‘c’, ‘d’, … ])로 매개변수들이 전달된다.

arguments에 접근하는 시점에 따라 값이 변경될 수 있다. Javascript의 parameter는 변경할 수 있기 때문에, 이를 변경 후 arguments를 찍어보면 다르게 나온다.

1
2
3
4
5
function hello(a, b) {
a = 1;
console.log(arguments);
}
hello('a', 'b'); // Arguments [1, 'b'];

this 객체

obj.prop()으로 호출 시 objthis가 된다. . 좌측의 객체가 항상 this가 된다.

최상단 scope에서 호출하면 기본적으로 window., global.이 생략된 것이기 때문에 thiswindow, global이 된다. (global은 Node.js 환경에서.)

const { prop } = obj; prop(); 하면 propobj에 속해있음에도 불구하고 thiswindow, global이 된다.

Function.prototype.call

this 객체를 지정해서 함수를 호출할 수 있다.

1
2
3
4
5
6
7
8
const obj = {
thisIs: 'obj',
};
function hello(a, b) {
console.log(this);
}
// 매개변수는 가변 매개변수여서 개수 제한 없이 전달 가능하다.
hello.call(obj, 'a', 'b'); // { thisIs: "obj" }

Function.prototype.apply

call과 다른 점은 매개변수를 배열과 유사한 객체로 넘겨야 한다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
thisIs: 'obj',
};
function hello(a, b) {
console.log(this);
}
// 매개변수는 배열과 같은 객체로 전달하면 된다.
hello.apply(obj, ['a', 'b']); // { thisIs: "obj" }
hello.apply(obj, {
0: 'a',
1: 'b',
length: 2,
}); // { thisIs: "obj" }

함수를 부분 실행하는 currying 개념

사용 예시 1

1
2
3
4
5
6
7
8
<Field
name="username"
onChange={handleChange('username')}
/>
<Field
name="password"
onChange={handleChange('password')}
/>

함수 본문 1

1
2
3
const handleChange = (fieldName) => (value) => {
fields[fieldName].value = value;
};

함수 본문 1 해설

함수 handleChangefieldName를 받아 새로운 함수를 반환한다. 반환한 함수에 value를 전달하는 경우, fields 객체에서 fieldName에 해당하는 프로퍼티에 value를 전달받은 value로 갱신한다.

사용 예시 2

1
2
3
4
5
6
const createForm = createElHtml('form');
const createInput = createElHtml('input');

const form = createForm();
const idInput = createInput('name="username"');
form.innerHTML = idInput;

함수 본문 2

1
2
3
4
const createElHtml = (tag) => (
arrtibutes = '',
children = '',
) => `<${tag} ${arrtibutes}>${children}</${tag}>`;

사용 예시 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const toBoolean = (x) => !!x;
const double = (n) => n * 2;
const toNumber = (str) =>
Number.parseInt(str, 10);
const composedFunction = compose(
toBoolean,
double,
toNumber,
); // toNumber -> double -> toBoolean 순으로 실행된다.
// 매개변수는 composedFunction에 전달된 것부터
// 각 함수의 반환값이 다음 함수의 매개변수가 된다.

composedFunction('0'); // false
composedFunction(''); // false
composedFunction(); // false
composedFunction('hello'); // false
composedFunction('2'); // true

함수 본문 3

1
2
3
4
const compose = (...functions) =>
[...functions].reduce((f, g) => (...args) =>
f(g(...args)),
);

함수 본문 3 해설

  • compose(f,g,h)(1) => f(g(h(1)))의 형태로 실행한다.
  • compose 내부에선 functions를 배열에 넣어 reduce를 호출한다. Array.reduce(result, currentvalue) => result; 이다. reduce에게 (f, g) => (...args) => f(g(...args))를 인자로 넘겨준다.
  • 단계별 흐름도
    • (f, g) => (...args) => f(g(...args)) // f, g
    • ((...args) => f(g(...args)), h) => ((...args) => f(g(h(...args)))) // f,g 합성함수, h 함수

함수형 프로그래밍과의 연관성

  1. currying함수를 인자로 받게 만들 수도 있고, 함수가 아닌 값을 받게 만들 수도 있다. 두 방법 모두 유용하다.
  2. currying은 마지막으로 반환되는 경우를 제외하면 대부분 계속해서 함수를 반환한다. 이는 함수간의 조합이 쉽다.

참고

  1. JavaScript ES6 curry functions with practical examples (EN)

  2. Learning Javascript Courses: ES6 Curry (EN)

  3. What is the advantage of currying? (EN)

실행 결과를 재사용하는 함수 skip

사용 예시

1
2
3
4
5
<ul id="list">
<li>Item 1<button>삭제</button></li>
<li>Item 2<button>삭제</button></li>
<li>Item 3<button>삭제</button></li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const askOnRemove = skip(() =>
confirm(
'정말 삭제하시겠습니까? 최초 1회만 확인합니다.',
),
);

const list = document.getElementById('list');
list.addEventListener('click', (e) => {
const {
nodeName,
parentNode: targetItem,
} = e.target;
const { parentNode } = targetItem;
nodeName === 'BUTTON' &&
askOnRemove() &&
parentNode.removeChild(targetItem);
});

함수 본문

1
2
3
4
5
6
function skip(callback) {
let result; // 클로저 영역
return function (...args) {
return result || (result = callback(...args));
};
}

함수 본문 해설

함수 skipcallback를 받아 실행하고, 만약 그 함수의 반환값이 truthy이면 해당 값을 반환하고, 이후에는 함수를 실행하지 않는다.

함수형 프로그래밍과의 연관성

  1. 함수형 자바스크립트는 함수를 N 단계로 조합해서 사용한다. 즉, 고차 함수 응용의 반복이다.
  2. skip은 고차 함수이고, callbackskip이 남겨 놓은 로직을 완성하는 함수다.
  3. 예시로 사용된 askOnRemove는 클로저인데, 함수로 만들어진 함수는 대부분 클로저다.

고차 함수란?

함수를 인수로 사용하거나 함수를 반환하는 함수이다.

예시:

  1. const filter = (predicate, xs) => xs.filter(predicate) // 함수가 인자
  2. const is = (type) => (x) => Object(x) instanceof type // 함수를 반환
  3. filter(is(Number), [0, '1', 2, null]) // [0, 2]