함수형 프로그래밍 (3)

Dan·2023년 5월 17일
0

실무공부

목록 보기
9/12
post-thumbnail

이터러블 중심 프러그래밍 실무에 적용해보기

앞서 만들어 놓은 여러가지 이터러블 프로토콜 기반의 함수들을 실무적인 코드에 적용하는 방법을 알아보도록 하자

  let users = [
    {
      name: 'a', age: 21, family: [
        {name: 'a1', age: 53}, {name: 'a2', age: 47},
        {name: 'a3', age: 16}, {name: 'a4', age: 15}
      ]
    },
    {
      name: 'b', age: 24, family: [
        {name: 'b1', age: 58}, {name: 'b2', age: 51},
        {name: 'b3', age: 19}, {name: 'b4', age: 22}
      ]
    },
    {
      name: 'c', age: 31, family: [
        {name: 'c1', age: 64}, {name: 'c2', age: 62}
      ]
    },
    {
      name: 'd', age: 20, family: [
        {name: 'd1', age: 42}, {name: 'd2', age: 42},
        {name: 'd3', age: 11}, {name: 'd4', age: 7}
      ]
    }
  ];

  go(users,
    L.flatMap(u => u.family),  // [{name: "a1" , age:53}, {name: "a2" , age:47}, ...]
    L.filter(u => u.age > 20),  // [{name: "a3" , age:16}, {name: "a4" , age:15}, ...]
    L.map(u => u.age), // [16, 15, ...]
    take(4), // [16, 15, 19, 7]
    reduce(add), // 57
    console.log
    );

비동기: 동기성 프로그래밍

자바스크립트에서 비동기 동기성 프로그래밍을 하는 방법은 크게 두가지이다.

  • callback 패턴
  • promise 패턴 (async await도 promise 기반)

Promise

일급

비동기 상황이 promise를 사용하면 일급,즉 값으로 다뤄질수 있기에 변수 또는 함수에 할당/전달될 수 있다는 점이 callback과 아주 큰 차이다.

  // callback 패턴
  function add10(a, callback) {
    // return을 안하고 있다
    setTimeout(() => callback(a + 10), 100);
  }

  let a = add10(5, res => {
    add10(res, res => {
      add10(res, res => {
        // log(res);
      });
    });
  });

  console.log(a) // 35

  // Promise 패턴
  function add20(a) {
    // return을 하고 있다
    return new Promise(resolve => setTimeout(() => resolve(a + 20), 100));
  }

  let b = add20(5)
    .then(add20)
    .then(add20)
    .then(console.log); // 65

일급 활용

프로미스가 일급 함수라는 점을 인지하고 활용해보도록 해보자

  const delay100 = a => new Promise(resolve =>
    setTimeout(() => resolve(a), 100));

  const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);
  const add5 = a => a + 5;

  const n1 = 10;
  console.log(go1(go1(n1, add5), log));

  const n2 = delay100(10);
  console.log(go1(go1(n2, add5), log));

함수 합성

모나드는 함수 합성을 안전하게 하기 위한 도구

  // f . g
  // f(g(x))

  const g = a => a + 1;
  const f = a => a * a;

  // 안전하지 않은 코드 합성
  console.log(f(g(1))); // 4
  console.log(f(g())); // 빈 값이 들어와도 함수를 실행해 NaN으로 평가된다

  // 모나드를 활용한 안전한 코드 합성
  Array.of(1).map(g).map(f).forEach(r => log(r)); // 4
  [].map(g).map(f).forEach(r => log(r)); //  빈 값이 들어오면 값이 평가 되지 않는다

  // Promise를 활용한 함수 합성
  // Promise는 then을 통해 함수를 함성한다, 하지만 프로미스는 비동기 상황에서의 안전한 합성을 한다.
  Promise.resolve(2).then(g).then(f).then(r => log(r)); // 4
  new Promise(resolve =>
    setTimeout(() => resolve(2), 100)
  ).then(g).then(f).then(r => log(r)); // 9

Kleisli Composition

오류가 있을 수 있는 상황에서의 함수 합성을 안전하게 할 수 있는 규칙

  // f . g
  // f(g(x)) = f(g(x))
  // f(g(x)) = g(x)

  var users = [
    {id: 1, name: 'aa'},
    {id: 2, name: 'bb'},
    {id: 3, name: 'cc'}
  ];

  const getUserById = id =>
    find(u => u.id == id, users) || Promise.reject('없어요!');

  const f = ({name}) => name;
  const g = getUserById;

  const fg = id => f(g(id));

  // catch로 인해 오류가 났을시 안전한 함수 합성이 가능하게 된다.
  const fg = id => Promise.resolve(id).then(g).then(f).catch(a => a);

  fg(2).then(log);

  setTimeout(function () {
    users.pop();
    users.pop();
    fg(2).then(log);
  }, 10);

go,pipe,reduce에서 비동기 제어

기존 go,pipe,reduce 함수는 promise 값을 받으면 제대로 작동이 안된다. 이 함수들을 promise도 받을 수 있도록 개선해보자.

const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);

const reduce = curry((f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  } else {
    iter = iter[Symbol.iterator]();
  }
  return go1(acc, function recur(acc) {
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      acc = f(acc, a);
      if (acc instanceof Promise) return acc.then(recur);
    }
    return acc;
  });
});

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

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

개선된 함수들을 사용해보면 정상적으로 작동 되는 것을 알 수 있다.

  go(Promise.resolve(1),
    a => a + 10,
    a => Promise.reject('error~~'),
    a => console.log('----'),
    a => a + 1000,
    a => a + 10000,
    log).catch(a => console.log(a));

Async-Await

await는 기본적으로 평가가 Promise를 내뱉는 함수 기반으로만 사용할 수 있다. 그렇기에 모든 비동기 프로그래밍에 async-await 문법이 만능템은 아닌 것이다. 즉 async-await를 써서 자유롭게 비동기 프로그래밍을 구현하고 싶다면 Promise를 리턴하는 함수를 직접 만들줄 알아야한다.

그리고 async 함수는 항상 Promise 값을 리턴하기에 리턴된 값을 평가받고 싶다면 await를 써야한다.

function delay(time) {
	return new Promise(resolve => setTimeout(() => resolve(), time)
}
                       
async function f1(){
    // await를 안쓰면 delay는 Promise를 리턴한다
	const a = delay(10); // Promise
    const b = await delay(10) // 10
    const c = await delay(10) // 10
    
    return b + c;
    console.log(a,b);
}
  
f1(); // Promise
  
f1().then(console.log); // 20
  
(async () => {
	console.log(await f1()) // 20
})(); 

Array.prototype.map vs FxJS의 map의 차이점

기본적으로 자바스크립트에서 지원되는데 map 메소드는 Promise로 리턴되는 값들을 다루지 못한다.

  function delayI(a) {
    return new Promise(resolve => setTimeout(() => resolve(a), 100));
  }

  async function f2() {
    const list = [1, 2, 3, 4];
    const temp = list.map(async a => await delayI(a * a));
    console.log(temp); // [Promise, Promise, Promise, Promise]
    const res = await temp;
    console.log(res); // [Promise, Promise, Promise, Promise]
  }

  f2();

  async function f3() {
    const list = [1, 2, 3, 4];
    const temp = map(a => delayI(a * a), list);
    console.log(temp); // Promise {<pending>}
    const res = await temp;
    console.log(res); //  [1,4,9,16]
  }

  f3();


  function f4() {
    return map(a => delayI(a * a), [1, 2, 3, 4]);
  }

  (async () => {
    console.log(await f4());
  })();

비동기는 async/await로 쉽게 제어할수 있는데 파이프라인이 왜 필요할까?

파이파라인과 async/await은 완전히 다른 장치이다.

  • 파이프라인은 함수를 합성하는게 목적이다.
  • async/await는 함수를 풀어 놓는게 목적이다. 비동기 상황을 동기적으로 표현하기 위한 장치
  // 파이프라인으로 코드 구현 시
  function f5(list) {
    return go(list,
      L.map(a => delayI(a * a)),
      L.filter(a => delayI(a % 2)),
      L.map(a => delayI(a + 1)),
      C.take(2),
      reduce((a, b) => delayI(a + b)));
  }

  go(f5([1, 2, 3, 4, 5, 6, 7, 8]), a => log(a, 'f5'));

  // 파이프라인 없이 코드 구현 시
  async function f6(list) {
    let temp = [];
    for (const a of list) {
      const b = await delayI(a * a);
      if (await delayI(b % 2)) {
        const c = await delayI(b + 1);
        temp.push(c);
        if (temp.length == 2) break;
      }
    }
    let res = temp[0], i = 0;
    while (++i < temp.length) {
      res = await delayI(res + temp[i]);
    }
    return res;
  }

  go(f6([1, 2, 3, 4, 5, 6, 7, 8]), log);

동기 상황에서의 에러 핸들링

  • 자바스크립트에서는 보통 try-catch문을 사용하여 에러 핸들링을 하면 된다.
  function f7(list) {
    try {
      return list
        .map(a => JSON.parse(a))
        .filter(a => a % 2)
        .slice(0, 2);
    } catch (e) {
      log(e);
      return [];
    }
  }
  log(f7(['0', '1', '2', '{']));

비동기 상황에서의 에러 핸들링

  • 비동기 상황에서는 async/await + try/catch를 함께 써서 처리할수 있다.
  async function f8(list) {
    try {
      return await list
        .map(async a => await new Promise(resolve => {
          resolve(JSON.parse(a));
        }))
        .filter(a => a % 2)
        .slice(0, 2);
    } catch (e) {
      // log(e, '----------------------');
      return [];
    }
  }
  f8(['0', '1', '2', '{']).then(log).catch(e => {
    log('에러 핸들링 하겠다.');
  });
profile
만들고 싶은게 많은 개발자

0개의 댓글