ES6+에서의 함수형 프로그래밍

윤상준·2023년 4월 30일
0

JavaScript

목록 보기
2/2
post-thumbnail

코드 평가

코드 평가
코드가 계산되어 값을 만드는 것을 의미한다.

일급 객체

일급 (First Class)
자바스크립트에서 일급이란 다음과 같은 의미가 있다.

  • 값으로 다룰 수 있다.
  • 변수에 담을 수 있다.
  • 함수의 인자로 사용될 수 있다.
  • 함수의 결과로 사용될 수 있다.

자바스크립트에서 일급 객체는 다음의 예시처럼 사용될 수 있다.

const a = 10;
const add10 = (a) => a + 10;
const r = add10(a);

일급 함수

자바스크립트에서 함수는 일급 객체에 속한다. 이와 관련된 내용은 모던 자바스크립트 딥다이브 정리에서 다룬 적 있다.

즉, 자바스크립트에서는 함수를 값으로서 다룰 수 있다. 이는 곧, 함수가 조합성과 추상화의 도구로 이용될 수 있음을 의미한다.

const add5 = (a) => a + 5;
log(add5);
log(add5(5));

const f1 = () => () => 1;
log(f1());

const f2 = f1();
log(f2);
log(f2());

고차 함수

고차 함수 (Higher order function)

  • 함수를 값으로서 다루는 함수
  • 함수를 인자로 받아서 실행할 수 있는 함수

다음의 예시처럼, 어떠한 함수를 값으로서 다루거나, 다른 함수를 인자로 받아 실행하는 함수를 고차 함수라고 한다.

const apply1 = (f) => f(1);
	const add2 = (a) => a + 2;
	log(apply1(add2));
	log(apply1((a) => a - 1));

	const times = (f, n) => {
		let i = -1;
		while (++i < n) f(i);
	};

	times(log, 3);

	times((a) => log(a + 10), 3);

또는 새로운 함수를 만들어서 반환하는 함수를 고차 함수라고 할 수 있다.

const addMaker = (a) => (b) => a + b;
const add10 = addMaker(10);
log(add10(5));
log(add10(10));

이터러블 / 이터레이터 프로토콜

이터러블 (Iterable)
이터레이터를 반환하는 [Symbol.iterator]() 를 가진 값
해당 객체들은 Symbol.iterator를 실행하면 이터레이터를 리턴한다.

이터레이터 (Iterator)
{value, done}의 객체를 반환하는 next()를 가진 값

이터러블은 다음과 같이 구현해볼 수 있다.
순회가 완료되기 전 까지는 done이 false이며 해당 인덱스의 value가 반환된다. 이후 순회가 완료되면 마지막 차례의 value와 함께 done이 true가 된다.

해당 순회는 next() 메소드로 한 차례식 호출할 수 있다.

const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i == 0 ? {done: true} : {value: i--, done: false};
      },
      [Symbol.iterator]() {
        return this;
      }
    }
  }
};
  
let iterator = iterable[Symbol.iterator]();
iterator.next();
iterator.next();
// log(iterator.next());
// log(iterator.next());
// log(iterator.next());
for (const a of iterator) {
  log(a);
}

제너레이터와 이터레이터

제너레이터 (Generator)

  • 이터레이터이자 이터러블을 생성하는 함수
  • 어떠한 값이든 순회가 가능하도록 만들 수 있다.
function* gen() {
  yield 1;
  if (false) yield 2;
  yield 3;
}

let iter = gen();

log(iter[Symbol.iterator]() == iter);
log(iter.next());
log(iter.next());
log(iter.next());
log(iter.next());

for (const a of gen()) {
  console.log(a);
};

map, filter, reduce

map

배열을 순회하여 새로운 배열을 생성하는 함수
배열의 순서와 길이가 보장된다.

map 함수는 이터러블 프로토콜을 따르기 때문에 다형성을 갖고 있다.

가령, document.querySelectorAll()의 경우 Array를 상속받지 않기 때문에 map 함수를 사용할 수는 없다. 하지만 이터러블 프로토콜을 따르고 있기 때문에 순회 자체는 가능하다.

const map = (f, iter) => {
  let res = [];
  for (const a of iter) {
		res.push(f(a));
	}
	return res;
};

console.log(map((el) => el.nodeName, document.querySelectorAll("*")));

제너레이터 또는 ES6의 Map 객체 역시도 이터러블 프로토콜을 따르고 있기 때문에 순회가 가능하다.

function* gen() {
	yield 2;
	if (false) yield 3;
	yield 4;
}

console.log(map((a) => a * a, gen()));

let m = new Map();
m.set("a", 10);
m.set("b", 20);

console.log(new Map(map(([k, a]) => [k, a * 2], m)));

filter

filter
배열을 순회하여 조건에 맞는 item만 골라낼 수 있는 함수
filter로 생성한 배열의 길이가 원본과 같다는 보장이 없다.

const filter = (f, iter) => {
	let res = [];
	for (const a of iter) {
		if (f(a)) res.push(a);
	}
	return res;
};

let under20000 = [];
for (const p of products) {
  if (p.price < 20000) under20000.push(p);
}

console.log(...under20000);
console.log(...filter((p) => p.price < 20000, products));

reduce

reduce
배열을 순회하여 하나의 값으로 누적하여 만들어가는 함수
단 하나의 값을 반환한다.

reduce에 함수를 인자로 전달하여 그 결과값을 누적하며 하나의 값으로 만들어낼 수도 있다.

const reduce = (f, acc, iter) => {
	for (const a of iter) {
		acc = f(acc, a);
	}
	return acc;
};

const add = (a, b) => a + b;
console.log(reduce(add, 0, [1, 2, 3, 4, 5]));
console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5));
const reduce = (f, acc, iter) => {
	if (!iter) {
		iter = acc[Symbol.iterator]();
		acc = iter.next().value;
	}
	for (const a of iter) {
		acc = f(acc, a);
	}
	return acc;
};

console.log(reduce((total_price, product) => total_price + product.price, 0, products));

map+filter+reduce 중첩 사용과 함수형 사고학습

함수형 프로그래밍의 핵심은 코드를 값으로 다룬다는 점이다.
즉, 값으로 평가될 것들을 함수로 만들어서 사용할 수 있다.

앞서 다뤘던 map, filter, reduce를 활용하면 다음과 같이 함수를 값으로서 사용할 수 있다.

const log = console.log;

const map = (f, iter) => {
	let res = [];
	for (const a of iter) {
		res.push(f(a));
	}
	return res;
};

const filter = (f, iter) => {
	let res = [];
	for (const a of iter) {
		if (f(a)) res.push(a);
	}
	return res;
};

const reduce = (f, acc, iter) => {
	if (!iter) {
		iter = acc[Symbol.iterator]();
		acc = iter.next().value;
	}
	for (const a of iter) {
		acc = f(acc, a);
	}
	return acc;
};
// index.js

const products = [
	{ name: "반팔티", price: 15000 },
	{ name: "긴팔티", price: 20000 },
	{ name: "핸드폰케이스", price: 15000 },
	{ name: "후드티", price: 30000 },
	{ name: "바지", price: 25000 },
];

const add = (a, b) => a + b;

log(
	reduce(
		add,
		map(
			(p) => p.price,
			filter((p) => p.price < 20000, products)
		)
	)
);

log(
	reduce(
		add,
		filter(
			(n) => n >= 20000,
			map((p) => p.price, products)
		)
	)
);

reduce의 경우, 두 번째 인자로 정수형의 배열이 올 것을 기대할 수 있다. 즉, 어떠한 방식으로던간에 정수형의 배열만 전달하면 된다는 의미.
이를 map 또는 filter 함수 자체를 전달하여 그 반환값을 정수형 배열로 사용할 수 있다.

즉, 함수의 인자로서 올 값을 무조건 이라고만 여기지 않고, 해당 값을 평가해낼 수 있는 표현식 또는 함수를 대신 사용할 수 있다.

이것이 바로 함수형 프로그래밍에서 코드를 값으로 다룬다는 것을 의미한다.

자바스크립트에서의 함수형 프로그래밍

자바스크립트에서 함수형 프로그래밍 하면 대표적으로 떠오르는 함수가 go, pipe 등이 있다.

go

go
reduce를 활용해서 여러가지 함수나 인자를 순서대로 중첩해가며 실행하여, 하나의 값으로 만들어내는 함수

go는 다음의 예시처럼 생성할 수 있다.

const go = (...args) => reduce((a, f) => f(a), args);

go(
  add(0, 1),
  (a) => a + 10,
  (a) => a + 100,
  log
);

이처럼 reduce의 인자로 초기값과 함수를 전달하여, 초기값에 함수를 적용하는 것을 시작으로 여러 함수를 차례로 적용한 다음, 마지막에 한 개의 값을 반환할 수 있다.

pipe

함수들이 나열되어 있는 합성된 함수를 만드는 함수

go는 값을 반환하지만 pipe는 함수를 반환하는 함수이다.
즉, pipe는 인자로 받은 함수들을 모두 합쳐서 합성된 함수를 반환한다.

const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);

go(
	add(0, 1),
	(a) => a + 10,
	(a) => a + 100,
	log
);

const f = pipe(
	(a, b) => a + b,
	(a) => a + 10,
	(a) => a + 100
);

log(f(0, 1));

출처 : 함수형 프로그래밍과 JavaScript ES6+

profile
하고싶은건 많은데 시간이 없다!

0개의 댓글