함수형 프로그래밍

Dan·2023년 5월 3일
0

실무공부

목록 보기
6/12
post-thumbnail

해당 내용은 인프런 "함수형 프로그래밍과 Javascript ES6+"라는 강의를 보며 개인적으로 정리하는 내용이다

함수형 자바스크립트 기본

평가와 일급

평가

코드가 계산 (Evaluation) 되어 값을 만드는 것

1 + 2
> 3 // 위에 계산이 3으로 평가 된다

[1, 2+3]
> [1, 5] // 위에 계산이 배열인 [1,5]로 평가 된다.

일급

  1. 값으로 다를 수 있다
  2. 변수에 담을 수 있다
  3. 함수의 인자로 사용될 수 있다
  4. 함수의 결과로 사용할 수 있다
const a = 10;
const add10 = a => a + 10;
const r = add10(a);
console.log(r) // 20

일급 함수

  1. 함수가 값으로 다뤄질 수 있다
  2. 조합성과 추상화의 도구
const add5 = a => a+5;
console.log(add5); // a => a+5
console.log(add5(5)); // 10

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

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

고차 함수

함수를 값으로 다루는 함수

  • 함수를 인자로 받아서 실행하는 고차 함수
const apply1 = f => f(1); // 고차 함수, f는 함수
const add2 = a => a + 2; // 일급 함수

console.log(apply1(add2));
console.log(apply1(a => a -1));

// 고차 함수,  f는 함수
const times = (f,n) => {
  let i = -1;
  while(++i < n) f(i);
}

times(console.log, 3);
times(a => console.log(a+10), 3);
  • 함수를 만들어 리턴하는 함수(클로저를 만들어 리턴하는 함수)
// 고차함수, 클로저를 리턴함
const addMaker = a => b => a+b;
const add10 = addMaker(10);

console.log(add10(5));
console.log(add10(10));

이터레이터 프로토콜

ES6에서의 순회와 이터러블

Array

  log('Arr -----------');
  const arr = [1, 2, 3];
  let iter1 = arr[Symbol.iterator]();
  for (const a of iter1) log(a);

Set

 log('Set -----------');
  const set = new Set([1, 2, 3]);
  for (const a of set) log(a);

Map

  log('Map -----------');
  const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
  for (const a of map.keys()) log(a);
  for (const a of map.values()) log(a);
  for (const a of map.entries()) log(a);
  console.clear();

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

  • 이터러블: 이터레이터를 리턴하는 Symbol.iterator를 가진 값
  • 이터레이터: {value, done} 객체를 리턴하는 next()를 가진 값
  • 이터러블/이터레이터 프로토콜: 이터러블을 for...of, 전개 연산자 등과 함께 동장하도록한 규약

사용자 정의를 통해 이터러블 정의하기

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();

for (const a of iterator) log(a); // 1



for (const a of document.querySelectorAll('*')) log(a);
const all = document.querySelectorAll('*');
let iter3 = all[Symbol.iterator]();
log(iter3.next());
log(iter3.next());
log(iter3.next());

제너레이터와 이터레이터

기존 배열 순회 방식

  • 해당 객체에 대한 length 정보가 필요하다.
  • index는 0 이상의 정수로써 순차적으로 증가해야 한다.
const array = [1,2,3];

for(let i = 0; i < array.length; i++){
  const element = array[i];
  
  console.log(element);
}

ES6에서의 또 다른 순회 방식

  • length 등의 데이터에 의존하지 않고 배열 자체로 순회할 수 있다.
const array = [1,2,3]

for(const element of array){
 	console.log(element); 
}

// 위의 for-of 이터레이터 인터페이스로 어떻게 구현되어 있는지 상상코드를 만들어보자
const iterable = [1,2,3];

const iterator = iterable[Symbol.iterator]();

while(true){
	const iteratorResult = iterator.next();
  	
  	if(iteratorResult.done) break;
  
  	console.log(iteratorResult.value);
}

// 1
// 2 
// 3

제너레이터 함수란?

  • 제너레이터: 이터레이터이자 이터러블을 생성하는 함수
  • 제너레이터 함수는 제너레이터 인스턴스를 생성한다
  • 제너레이터 인스턴스는 iterable 인터페이스와 iterator 인터페이스를 동시에 구현한다

제너레이터 문법

  • function*: 제너레이터 함수 선언 키워드
  • yield: next 메서드 호출 시, value로 반환할 값에 사용하는 키워드. 다음 next가 호출될 때까지 해당 키워드에서 함수 실행을 멈춘다.
  • yield*: 또 다른 iterable 객체를 위임해서 사용하기 위한 키워드

1. 기본적인 제네레이터 사용법

  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()) log(a);
  console.clear();

위와 같이 조건문을 넣어서 제너레이터는 문장을 통해 값을 만들 수 있다. 즉 제너레이터는 굉장히 다양한 값들을 로직을 만들어가며 순회 시킬 수 있도록 할 수 있는 강력한 도구이다.

2. yield* 활용 예시

// yield* 사용
function* generatorFunction() {
	yield* [1,2,3];
}

// yield* 풀어서 보기
function* generatorFunction2() {
	for(const element of [1,2,3]) {
    	yield element;
    }
}

const generator = generatorFunction();

for(const element of generator){
 	console.log(element); 
}

// 1
// 2
// 3

Lazy Evaluation

계산의 결과값이 필요할 때까지 계산을 늦추는 기법 (필요한 데이터를 필요한 순간에 생성한다.)

제너레이터를 좀 더 이해하기 위해 제너레이터를 통해서 홀수만 순회하며 생성하는 함수를 만들어보자

function* infinity(start = 0) {
  let i = start;
  while (true) {
    yield i++;
  }
}

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

function* odds(limitValue) {
  for (const item of limit(infinity(1), limitValue)) {
    if (item % 2 === 1) {
      yield item;
    }
  }
}

const iter = odds(10);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);

for (const item of odds(40)) {
  console.log(item);
}

Early Evaluation과 Lazy Evaluation의 성능차이

  1. Lazy Evaluation 활용 (총 2번 순회)
Object.prototype.lazyMap = lazyMap;
Object.prototype.take = take;

function* lazyMap(callback) {
	for(const el of this){
    	yield callback(el);
    }
}

function take(n = Infinity){
	const result = [];
  
  	for(const el of this){
    	result.push(el);
      
      	if(result.length === n) break;
    }
  
  	return result;
}

function callback(element){
	console.log("callback called");
  
  	return element + 1;
}

const arr = Array(10000).fill(1);

const mappedArr == arr.lazyMap(callback).take(2);

console.log(mappedArr);

// 총 필요한 2번만 순회를 한다.
  1. Early Evaluation 활용 (총 만번 순회)
const arr = Array(10000).fill(1);

const mappedArr == arr.map(callback).take(2);

console.log(mappedArr);

map, filter, reduce

map 구현 전

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

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

map 구현 후

뽑아내고 싶은 데이터를 함수로 전해줌으로써 코드를 간결하게 만들 수 있다. map은 f라는 함수를 인자값으로 받고 있기 때문에 고차함수 개념이 적용된다.

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

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

이터러블 프로토콜을 따른 map의 다향성

  log([1, 2, 3].map(a => a + 1));

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

  // const it = document.querySelectorAll('*')[Symbol.iterator]();
  // log(it.next());
  // log(it.next());
  // log(it.next());
  // log(it.next());
  // log(it.next());

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

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

  let m = new Map();
  m.set('a', 10);
  m.set('b', 20);
  log(new Map(map(([k, a]) => [k, a * 2], m)));

filter

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

  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);
  // }
  // log(...under20000);

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

  // let over20000 = [];
  // for (const p of products) {
  //   if (p.price >= 20000) over20000.push(p);
  // }
  // log(...over20000);

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

  log(filter(n => n % 2, [1, 2, 3, 4]));

  log(filter(n => n % 2, function* () {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
  }()));

reduce

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

 const nums = [1, 2, 3, 4, 5];

  let total = 0;
  for (const n of nums) {
    total = total + n;
  }
  log(total);

  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;

  // 기본적인 리듀스 함수 사용
  log(reduce(add, 0, [1, 2, 3, 4, 5]));
  // 15

  // 리듀스를 사용하지 않았을 경우
  log(add(add(add(add(add(0, 1), 2), 3), 4), 5));
  // 15

  // acc 값을 안넣엇을 경우
  log(reduce(add, [1, 2, 3, 4, 5]));
  // 15
profile
만들고 싶은게 많은 개발자

0개의 댓글