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

O0·2022년 6월 28일
1

Javascript ES6+

목록 보기
1/2
post-thumbnail

함수형프로그래밍 & Javascript ES6+


1. 함수형 자바스크립트 기본기

스터디 소개

  • ES6+의 이터러블/이터레이터/제너레이터 프로토콜을 상세히 다루고 응용한다.
  • 이터러블을 기반으로한 함수형프로그래밍, map/filter/reduce, 파이프라인, 재너레이터를 통한 지연 평가, Promise 합성, 동시성/병렬성, async/await, 비동기 에러 핸들링 등을 배울 수 있다.
  • ES6를 써야하는 이유는 코드의 가독성 때문이다.
    • 아름다운 코드는 짧고 간결해야 한다.
    • 회사 지원자격에 ES6+에 대한 깊은 이해가 있으신 분 을 요구하는 경우가 많다.

ES6+

  • ES란 ECMAScript의 약자이며 자바스크립트 표준, 스펙을 나타내는 용어다.
  • ES6+는 2015년에 개정된 javascript 스펙 ES6를 포함한 상위 스펙을 통칭하는 말이다.

평가와 일급

  • 평가 : 코드가 계산 되어 값을 만드는 것
    • [1,2 [3,4]]
      -> (3) [1,2 Array(2)] 이런식으로 평가 된다.
  • 일급
    • 값으로 다룰 수 있다.
    • 변수에 담을 수 있다.
    • 함수의 인자 및 결과로 사용될 수 있다.
    • 람다의 특징 중 하나
const a = 10;
const add10 = a => a+10;
const r = add10(a);
	log(r); // 20

일급 함수

  • 함수를 값으로 다룰 수 있다.
  • 조합성과 추상화의 도구
const add5 = a => a+5;
log(add5); //a => a+5
log(add5(5)); // 10

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

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

고차함수

  • 함수를 값으로 다루는 함수
    1) 함수를 인자로 받아서 실행하는 함수
    • apply1
    • times
	const apply1 = f => f(1);
	const add2 = a => a+2;
	console.log(apply1(add2)); //3
	console.log(apply1(a=>a-1)); //0
	
	const times = (f,n) => {
		let i = -1;
		while(++i < n) f(i);
	};
	
	times(console.log, 3); // 0 1 2
	times(a => console.log(a + 10),3)
	//10 11 12
  • 함수를 만들어 리턴하는 함수(클로저를 만들어 리턴하는 함수)

    const addMaker = a => b => a + b; //클로저를 리턴하는 함수
    const add10 = addMaker(10);
    console.log(add10(5)); // 15 
    console.log(add10(10)); // 20
    //클로저는 내부함수가 외부함수의 맥락에 접근할 수 있는 것을 가르 킨다.
    // 내부함수(addMaker 10 => a + 10 을 리턴)가 외부함수의(add10(5)에서 5가 외부함수의 맥락이다.) 맥락에 접근한다.




2. ES6에서의 순회와 이터러블:이터레이터 프로토콜

리스트 순회

  const list = [1,2,3];
  for(var i=0; i<list.length; i++){
    	console.log(list[i]);                         
	}
// ->
  for(const a of list){
  	console.log(a);
  }

Array, Set, Map을 통해 알아보는 이터러블/이터레이터 프로토콜

  • Array, Set, Map은 자바스크립트 내의 내장객체.
  • 이터러블/이터레이터 프로토콜 (자바랑 비슷)
    • 이터러블 : 이터레이터를 리턴하는 Symbol.iterator 를 가진 값 (ARRAY,SET,MAP)
    • 이터레이터: {value, done} 객체를 리턴하는 next()를 가진 값 (ITERATOR)
    • 이터러블/이터레이터 프로토콜 : 이터러블을 for...of, 전개 연산자 등과 함께 동작하도록한 규약 (DOM, 페이스북 API, 웹 API 등에서 사용)
const arr = [1,2,3];
console.log(arr[Symbol.iterator]);
// 결과(f values()) array는 Symbol.iteraotr를 갖고 있고
let iterator = arr[Symbol.iterator]();
for(let a of iterator) console.log(a); // 1,2,3
  
/*
 * Object done: false value: 1[[Prototype]]: Object
 Object done: false value: 2[[Prototype]]: Object
 Object done: false value: 3[[Prototype]]: Object
 */

전개연산자

  • 배열 또는 객체를 하나하나 넘기는 용도로 사용된다.
const a = [1,2];
console.log([...a,...[3,4]]);
//1234
//console.log([...a,...arr,...set,...map.keys()]);
//iterator 프로토콜을 따르고 있는 것.

재너레이터

  • 제너레이터 : 일반 함수는 하나의 값(혹은 0개의 값)만을 반환합니다. 하지만 제너레이터를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있습니다. 재너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 스트림을 만들 수 있습니다.
    • 제너레이터를 만들려면 '제너레이터 함수'라 불리는 특별한 문법 구조 function *이 필요합니다.
    • yield : return 처럼 동작하며 정해진 구간을 순서대로 실행하며 반드시 'function*'을 사용한 함수에서 사용한다. next()를 호출해야 가장 가까운 yield 문을 만나 실행을 한다.
 function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, 2가 출력됨
}
  function *gen() {
	yield 1;
	if (falses) yield 2;
	yield 3;
}//재너레이터 : 순회할 수 있는 값이면 어떠한 값이든 이터러블을 만들 수 있다.

let iter = gen(); // 이터레이터를 리턴하는 이터러블 만들기
console.log(iter.next()); // value: 1, done: false}
console.log(iter[Symbol.iterator]()); // iterator는 자기자신을 리턴한다.
//**********************************//
  
function *infinity(i = 0){
	while(true) yield i++;
}

function *limit(l, iter) {
	for(const a of iter){
		yield a;
		if (a == l) return;
	}
}

let iter1 = limit(4,[1,2,3,4,5,6]);
console.log(iter1.next()); //1
console.log(iter1.next()); //2
console.log(iter1.next()); //3
console.log(iter1.next()); //4
console.log(iter1.next()); //undefined
console.log("=========================")

function *odds(l) {
	for(const a of limit(l,infinity(1))) {
		if (a % 2 ) yield a;
	}
}

let iter2 = odds(10);
console.log(iter2.next()); //1
console.log(iter2.next()); //3
console.log(iter2.next()); //5
console.log(iter2.next()); //7
console.log(iter2.next()); //9
console.log(iter2.next()); //undefined 쭉
  
console.log(...odds(10)); // 1 3 5 7 9
console.log([...odds(10), ...odds(20)]); // Array(15) 

const[head, ...tail] = odds(5);
console.log(head); // 1
console.log(tail); // array(2) (3 5)


//재너레이터 코드를 순회해서 로직을 만들고 문장으로 만들어진 것을 순회하고 값으로 다룰 수 있다.

3. map, filter, reduce

map

<script type="text/javascript">
let products =[
	{name: '반팔티', price: 15000},
	{name: '긴팔티', price: 15000},
	{name: '모자', price: 15000},
	{name: '핸드폰케이스', price: 15000},
	{name: '후드티', price: 15000},
	
];
</script>
<script>
let map = (f,iter) => {
	let res = [];
	for(const a of iter) {
		res.push(f(a));
	}
	return res;
};

console.log(map(p => p.name, products));
console.log(map(p => p.price, products));
//함수형 프로그래밍으로 map 구현

 // let names = [];
 // for (const p of products) {
 //   names.push(p.name);
 // }
 // log(names);

 // let prices = [];
 // for (const p of products) {
 //   prices.push(p.price);
 // }
 // log(prices);
 
let m = new Map();
m.set('a',10);
m.set('b',20);

console.log(new Map(map(([k,a]) => [k,a*2],m))); // value 값에 2 곱해진 map 리턴

filter

<script type="text/javascript">
let products =[
	{name: '반팔티', price: 15000},
	{name: '긴팔티', price: 10000},
	{name: '모자', price: 8000},
	{name: '핸드폰케이스', price: 7000},
	{name: '후드티', price: 12000},
	
];
</script>

<script type="text/javascript">
const filter = (f,iter) => {
	let res = [];
	for(const a of iter){
		if(f(a)) res.push(a);
	}
	return res;
};

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

console.log(...under10000);
console.log(...filter(p => p.price<10000, products));
</script>

reduce

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;
};

const add = (a,b) => a + b;
console.log(reduce(add,0,[1,2,3,4,5])); //15
console.log(reduce(add,[1,2,3,4,5])); // 15
console.log(add(add(add(add(add(0,1),2),3),4),5)); //15
  
console.log(
		reduce(
		add,
		map(p => p.price,
				filter(p => p.price < 20000, products))));

4. 코드를 값으로 다루어 표현력 높이기

go

  • 함수들과 인자를 즉시 평가하는데 사용.
const go = (...args) => reduce((a, f) => f(a), args); 

go(
	/* 0,
	a => a + 1, */
	add(0, 1),
	a => a + 10,
	a => a + 100,
	console.log
	//111
);

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

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

pipe

  • 함수를 리턴하고 나열되어 있는 함수들을 합성한다.
  • go함수를 활용하여 만든 함수
const pipe = (f,...fs) => (...as) => go(f(...as),...fs);
const f = pipe(
		//a => a + 1,
		(a,b) => a+b,
		a => a + 10,
		a => a + 100
		);
console.log(f(0,1));

curry

  • 함수를 값으로 다루면서 받아둔 함수를 내가 원하는 시점에
    평가시키는 것이다.
  • 함수를 받아서 함수를 리턴하고 인자를 받아서 내가 원하는 수만큼 들어 왔을 때 작업을 완료하는 함수이다.
      1. 함수를 받아서 함수를 리턴한다.
      1. 리턴된 함수가 실행됐을 때 인자가 2개 이상이면 받아둔 함수를 즉시 실행한다.
      1. 2개보다 작으면 함수를 다시 리턴한 후에 그 이후에 받은 인자들을 합쳐서 실행한다.
  const curry = f => 
  (a, ..._) => _.length ? f(a,..._) : (..._) => f(a, ..._);
  const multi = curry((a,b) => a*b);
  console.log("curry ex : " + multi(3));
  // curry ex : (a, ..._) => _.length ? f(a,..._) : (..._) => f(a, ..._)
  console.log("curry ex : " + multi(3)(2));

go, pipe의 사용 이유는 각종 함수를 한 번에 묶고 가독성을 높일 수 있어서 사용한다.

5.장바구니 예제

//js.js
const log = console.log;

const curry = f =>
  (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);

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

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

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

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

const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>HTML 출력해보기 - 장바구니</title>
  <script src="./js.js"></script>
  
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH91sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
  
</head>
<body>

<div id="cart">
</div>

<script>
  const products = [
    {name: '반팔티', price: 15000, quantity: 1, is_selected: true},
    {name: '긴팔티', price: 20000, quantity: 2, is_selected: false},
    {name: '핸드폰케이스', price: 15000, quantity: 3, is_selected: true},
    {name: '후드티', price: 30000, quantity: 4, is_selected: false},
    {name: '바지', price: 25000, quantity: 5, is_selected: true}
  ];

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

  const sum = curry((f, iter) => go(
    iter,
    map(f),
    reduce(add)));

  const total_quantity = sum(p => p.quantity);
  const total_price = sum(p => p.price * p.quantity);
  
  document.querySelector('#cart').innerHTML = `
    <table class="table table-hover">
      <tr class="table-info">
        <th></th>
        <th>상품 이름</th>
        <th>가격</th>
        <th>수량</th>
        <th>총 가격</th>
      </tr>
      ${go(products, sum(p => `
          <tr class="table-light">
            <td><input type="checkbox" ${p.is_selected ? 'checked' : ''}></td>
            <td>${p.name}</td>
            <td>${p.price}</td>
            <td><input type="number" value="${p.quantity}"></td>
            <td>${p.price * p.quantity}</td>
          </tr>
      `))}
      <tr class="table-primary">
        <td colspan="3">합계</td>
        <td>${total_quantity(filter(p => p.is_selected, products))}</td>
        <td>${total_price(filter(p => p.is_selected, products))}</td>
      </tr>
    </table>
  `;
  
// 문자열이 배열에 있었는데 reduce를 통해서 하나의 문자열로 더해주게 되면 
// console과 같이 테이블형태의 string이 생긴다.
//go를 사용해서 함수를 나열하고, pipe를 사용해서 함수를 묶고, curry는 메소드 참조를 할 수 있도록 해준다.
//총 수량
/* const total_quantity = pipe(
	map(p => p.quantity),
	reduce(add)) */
//총 합계
/* const total_price = pipe(
		map(p => p.price),
		reduce(add)); */

</script>

</body>
</html>

profile
O0

0개의 댓글