[FP] 함수형 프로그래밍 with lodash

colki·2021년 4월 19일
0

[Inflearn] 자바스크립트로 알아보는 함수형 프로그래밍 강의 내용을 바탕으로 작성한 학습 메모입니다.


reduce funtion

객체지향 / 명령형 프로그래밍에서는 객체가 가장 앞에 오지만
순수함수로 구현할 때는 함수가 먼저 오고 인자에 객체와 보조함수(콜백함수)가 들어온다.

_reduce

// 순수함수로 _reduce 함수를 만들고나서, 실행한다면 이런 형태이다. 

_reduce([1, 2, 3], add, 0);

// add 자리에는 아래와 같은 add 기능을 하는 보조함수가 들어가고
// 초기값은 0이니 0부터 시작해서 배열의 요소들을 하나씩 더해나갈 예정이다.

function(a, b) { return a + b; }

// 최종적으로 리턴하게 될 축약된 값을 memo 변수에 할당한다면
// reduce의 프로세스는 아래와 같다.

memo = add(0, 1); // 초기값 0 + 배열[0]
memo = add(memo, 2); // 누적값 memo + 배열[1]
memo = add(memo, 3); // 누적값 memo + 배열[2]

return memo;

//만약 배열의 10개라면 10줄이 될 것이다.
// 커링함수를 이용해서도 함수가 적용되는 과정을 표현할 수 있다.

function _curryr(fn) {
  return function(a, b) {
    return arguments.length === 2
    ? fn(a, b)
    : function(b) { return fn(b, a); }; 
  };
}

var add = _curry(function(a, b) {
  return a + b;
});

add(add(add(0, 1)), 2), 3); // 이렇게 중첩해서 계속 값이 축약된다.

배열을 돌면서 각 요소마다 함수가 실행되는 거라
먼저 배열을 순회부터 해야 하는데 배열순회 또한 순수함수로 구현하고 나서 reduce에 넣어주면 코드를 간결하게 할 수 있다. 배열 한 바퀴 도는_each라는 함수를 먼저 만들어보자.


// 인프런 강의에서의 each

function _each(list, iterator) {
  for (let i = 0; i < list.length; i++) {
    iterator(list[i]);
  }
  return list; // list[i]마다 함수를 돌림
}


// 아래는 lodash_collection method 를 구현할 때 만들었던 -.each이다. 
// 본 포스팅에서는 위 _each만으로 충분하다.

_.each = function (collection, iterator) {
  if (Array.isArray(collection)) { // 배열일 때
    for (let i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else { //객체일 때
    for (const [key, value] of Object.entries(collection)) {
      iterator(value, key, collection);
    }
  }
};

😃 이제 함수를 도는 로직을 reduce안에 넣어서 거대하게 만들지 않아도 된다.
_each를 적용해서 _reduce 로직을 완성해보자.

function _reduce(list, iterator, memo) {
  _each(list, function(val) {
    memo = iterator(memo, val);
  });

  return memo;
}

console.log(
  _reduce([1, 2, 3], function(a, b) {
    return a + b;
  }, 0)); // 6
// add기능을 하는 콜백함수가 갂 요소마다 실행되어서
// 예상했던 6이라는 값이 나온다.


_reduce([1, 2, 3], function(a, b){return a + b; }) // NaN
//하지만 마지막 memo 인자에 아무값도 주어지지 않는다면 값이 나오지 않는다.

처음에 reduce 메서드에 대해서 언급한 대로, 초기값이 없을 경우
주어진 배열의 첫 번째 요소[0]를 초기값으로 써야 하는 메서드인데,
위에서 구현한 함수에는 초기값 memo가 undefined일 예외사항에 대해서 처리해준 게 없다.

초기값이 없는데 첫 번째 요소를 초기값으로 설정??!!
이 부분이 처음에 잘 와닿지 않았는데 아래 프로세스를 보고 명확히 뇌에 각인됐다.


//앞에서 했던 방식으로 다시 보면 마지막 memo값이 없다면 다음과 같이 동작한다.

_reduce([1, 2, 3], add); // 마지막놈 어디갔니, 안되겠다 1 너가 드루와!

memo = add(1, 2); // 요소의 첫 번째 인자가 memo로 들어가서 '초기값'이 된다.
memo = add(memo, 3);

return memo;

add(add(1, 2), 3); // 아까보다 껍데기(?)가 하나 줄었다.

memo 인자가 없다면? 즉 undefined일 때 예외처리 구문을 추가해주면 다음과 같다.

function _reduce(list, iterator, memo) {

  if (arguments.length === 2) { // 인자가 2개만 있을 경우
    memo = list[0];
    list = list.slice(1);
  }
  _each(list, function(val) {
    memo = iterator(memo, val);
  });

  return memo;
}


/* 하지만 매개변수에 재할당하는 것은 지양해야 하는 부분이고, 
또한 세 번째 인자가 undefined이기도 하므로 
위 코드는 아래와 같이 변경해볼 수 있다.*/

function _reduce(list, iterator, memo) {
  let result = memo;
  let newList = list;
  
  if (result === undefined) { 
    result = list[0];
    newList = list.slice(1);
  }
  _each(list, function(val) {
    result = iterator(result, val);
  });

  return result;
}

여기까지 해도 큰 문제는 없으나, 순수함수는 배열이나 유사배열, 객체를 넣더라도
제 기능을 이행해야 한다. 그런데 위에서 slice는 배열메서드이기 때문에
만약 유사배열이나 객체분이 입장한다면 무척이나 곤란하다.

그래서 그럴 경우에는 배열로 바꿔서 적용되도록 수정해주어야 한다.
Array.prototype.slice를 사용해서 바꿔줄 수 있는데 이것도 작은 함수로 나눈다면

const slice = Array.prototype.slice // slice변수에 할당

function _rest(list, num = 1) { // default parameter
  return slice.call(list, num); // .call **
}                              

function _reduce(list, iterator, memo) {
  let result = memo;
  let newList = list;
  
  if (result === undefined) { 
    result = list[0];
    newList = _rest(list) //이렇게 넣어줄 수 있다.
  }
  _each(list, function(val) {
    result = iterator(result, val);
  });

  return result;
}

pipe funtion

함수들을 인자로 받아서 연속적으로 실행되도록 하는 함수이며
리턴값 또한 함수인 파이프함수는 reduce에 특화된 함수이다.

_pipe

오직 인자로 함수들만을 받는 함수로써,
var f1 = _pipe(함수1, 함수2..)
f1(인자);
이런 식으로 실행하는 정말 이상한 ...함수이다.

함수를 인자로 주거니 받거니 하는 방식은 쌉초보인 나에게는 아직도 많이 어려운 부분이라 머리와 심장이 동시에 이해하는데 시간이 좀 걸린다..


var f1 = _pipe(
  function (a) { return a + 1; }, // 1 + 1 = 2
  function (a) { return a * 2; }  // 2 * 2 = 4
);
console.log( f1(1) );

// _pipe함수를 이용해서 위와 같은 결과를 만들고 싶다.
// 첫 번째 함수가 뱉은 값을 두 번째 함수가 받아 먹고 축약된 값을 내미는 상황이 정말 reduce 그 자체이다. 
// _pipe함수의 모습은 다음과 같다.

function _pipe() {
  var funcs = arguments;

  return function (arg) {
    return _reduce(funcs, function(arg, func) { 
      return func(arg); 
    }, arg);
  };
}

리듀스 기본
_reduce(list, iterator(함수),memo(초기 축약값))
mem0 = iterator(memo, val)

파이프 리듀스
_reduce(funcs 함수배열, 함수, arg(초기 축약값인 함수))
arg = 함수(arg, func)

파이프 함수 내부에서 리턴되는 리듀스는 기본 리듀스와 구조가 똑같다. 다만 함수일 뿐.. :(


function _pipe() {
  var funcs = arguments; //[함수1, 함수2..] 함수 배열 

  return function (arg) { 
  // a의 값, 함수1, 2들이 받아먹는 인자, reutrn +함수(함수리턴하는 애니까), 리듀스로 나올 함수가 여기 담긴다.
  
    return _reduce(funcs, function(arg, func) { 
      return func(arg); 
    }, arg); 
  };
}

var f1 = _pipe(
  function (a) { return a + 1; }, // 1 + 1 = 2 ----함수[0]
  function (a) { return a * 2; }  // 2 * 2 = 4 ----함수[1]
);

f1(1);

pipe함수에 들어온 a = 1 (arg)이 초기값(memo, accumulater)으로 설정된다
그리고 함수들의 배열로 입장하는데,
함수[0]이 1을 반죽해서 2로 리턴하면, 그걸 그다음 함수[1]이 2로 받아서
요리해서 4로 뱉어내는 것이다.

함수 배열안에 함수요소들이 더 많다면 반죽하고 밀대로 밀고 튀기고 굽고 찌고 삶는 등 별 짓을 다 할 것이고, 이렇게 해서 arg 는 완성된 arg 모습(함수)으로 리턴될 것이다. RGRG

_go

_pipe함수의 즉시 실행 버전으로,
첫 번째 인자는 함수를 받지 않고 두 번째 부터를 함수들로 받는 함수이다. 그래서 첫 번째 인자를 제외한 인자들을 함수의 배열로 선언한 뒤 _pipe함수를 재사용한다.

//_redcue편에서 만들었던 _rest함수를 여기에 사용할 수 있다.
const slice = Array.prototype.slice

function _rest(list, num = 1) {
  return slice.call(list, num);
}

/******* _go *******/
function _go(arg) {
  var funcs = _rest(arguments);
  return _pipe.apply(null, funcs)(arg);
}

// _pipe함수를 실행하는데, 함수배열을 인자로 넘겨준다.
// 

_pipe.apply(null, funcs)(arg); 이 부분이 조금 헷갈릴 수 있는데 이것은 위 pipe 함수에서 보았던 프로세스와 똑같다. 코드로 나타내면 다음과 같다.

// 아래 인자들을 넘겨서 _go로 실행한다 했을 때

_go(1, // 첫 번째 인자는 함수가 아닌 1
  function (a) { return a + 1; },
  function (a) { return a * 2; },
  function (a) { return a * a; },
  console.log
);
  
// 1을 제외한 함수들이 funcs 배열의 요소가 된다.
// 다시 _go 함수의 로직을 보자.

function _go(1) {
  var funcs = _rest(arguments);
  return _pipe.apply(null, funcs)(1);
}

_pipe.apply(null, funcs)(1);
결국 이 한 줄이 리턴되는데, 이 한줄을 변수에 담아서 실행하는 모습으로 보면 무릎을 탁 칠 수 있다.

const goFuncs = _pipe.apply(null, funcs);

/*  위 한줄의 코드는 아래와 똑같은 것이다.
const goFuncs = _pipe(
  function (a) { return a + 1; },
  function (a) { return a * 2; },
  function (a) { return a * a; },
  console.log
); */

goFuncs(1);

each function

2.2 강의에서 _each를 함수형프로그래밍으로 구현하고 이를 응용해서
map, filter등을 구현해왔다. 물론 배열에만 적용했기 때문에 과정 중에서는 에러는 없었으나

function _each(list, iterator) {
  for (let i = 0; i < list.length; i++) {
    iterator(list[i]);
  }
  return list; // list[i]마다 함수를 돌림
}

_each(null, console.log) // TypeError

lodash의 _.each에서 배열일때와 아닐때를 나눠서 로직을 짜봤기 때문에
인프런에서의 위 코드는 배열에 한정된 로직이라 의구심을 갖고 있었다.

하지만, 다형성 강의에서 리팩토링해서 실용성과 다양성을 갖춘 로직으로
재탄생되는 걸 보면서 내가 생각했던 방식과 전혀 달러서 새롭기도하고 재밌어서 기록을 남기려고 한다.

위에서 null 이 들어가게 되면 TypeError가 난다. 이부분을 먼저 수정해보자.

강의에서는 _get 함수를 이용해서 null 일경우에 대한 예외처리를 했는데
이상하게 똑같이 따라해도 length is not a function 에러가 나서
예외처리 코드를 따로 추가해봤다.

function _each(list, iterator) {
  if(list === null) { 
    return undefined
  }

  for (let i = 0; i < list.length; i++) {
    iterator(list[i]);
  }
  return list; 
}

list가 null 일경우 undefined를 리턴하도록 구현했다.

_each(null, console.log)
// undefined
// 예상대로 에러가 아닌 undefined가 나온다.

_each({
  13: 'ID',
  19: 'HD',
  29: 'YD'
}, function(name) {
  console.log(name);
});
// 텅텅

null에 대한 예외처리는 해줬지만, 객체가 들어올 경우도 생각해야 한다.
객체에는 length가 없기 때문에 코드는 제대로 작동하지 않는다.

console.log([1, 2, 3].length); // 3

console.log({
  13: 'ID',
  19: 'HD',
  29: 'YD'
}.length);
// undefined

🔅 solution

객체도 배열처럼 각 값들의 length를 가져온다면
기존 코드를 공통으로 사용할 수 있을 것이다. 그러면 어떻게 해주면 좋을까?


Object.keys() 를 이용하면 배열에서는 인덱스
객체에서는 key/프로퍼티를 배열형태로 추출할 수 있다.

/* Array */
Object.keys(['a', 'b', 'c', 'd']);  //  ["0", "1", "2", "3"]

/* Object */
Object.keys({
  13: 'ID',
  19: 'HD',
  29: 'YD'
}); // ["13", "19", "29"]

그러면 배열처럼 length를 뽑아낼 수 있게 되므로 다음과 같이 함수를 만들 수 있다.

function _is_Object (obj) {
  return typeof obj === 'object' && !!obj
}

function _keys(obj) {
  return _is_Object(obj)
    ? Object.keys(obj)
    : [];
}

// 자 그럼 _each도 아래에 다시 작성해보자

function _each(list, iterator) {
  var keys = _keys(list); // 배열과 객체에서 keys 추출

  if (list === null) {  
    return undefined
  }

  for (let i = 0; i < keys.length; i++) { //keys.length
    iterator(list[keys[i]]); // list의 [i]번째 key
  }
  return list;
  
  
  // 앞서 확인하지 못했던 객체를 넣어보면 제대로 나오는 것을 확인할 수 있다.
  
  _each({
  13: 'ID',
  19: 'HD',
  29: 'YD'
}, function(name) {
  console.log(name);
}); 
// ID 
// HD
// YD

😃 반면 collection을 인자로 받는 lodash _.each에서는 아래와 같이
Array.isArray로 판별 후 각각의 key를 추출하는 로직을 다르게 구분하였다..

_.each = function (collection, iterator) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
    return;
  }

  for (const [key, value] of Object.entries(collection)) {
    iterator(value, key, collection);
  }
};```



profile
매일 성장하는 프론트엔드 개발자

0개의 댓글