All Articles

유인동님의 함수형 프로그래밍 - 순수함수, 일급함수, currying, reduce

image.png

성공적인 프로그래밍이란?

  • 사용성, 성능, 확장성, 기획 변경에 대한 대응력이 좋은 것..!

함수형 프로그래밍

  • 성공적인 프로그래밍을 위해 부수 효과보다는 조합성을 강조하는 프로그래밍 패러다임
  • 부수효과를 지양하는 것 => 순수함수 만드는 것
  • 조합성을 강조하는 것 => 모듈화 수준을 높이는 것
  • 순수함수 => 오류를 줄이고 안정성을 높이는 것. 다시 말해
    동일한 인자를 주면 동일한 결과를 리턴하는 것(부수효과가 없음)
  • 모듈화 수준 높아짐 => 생산성 높이는 것

ex)

const add = (a,b) => a+b
add(10,5)

let c= 20;
const add2 = (a,b) => a+b+c;
add2(10,5)

console.log(c,'c 이전값')
const add3 = (a,b) => {
  c=b;
  console.log(c, 'c 이전값')
  return a+b
}

add3(20,30)

위의 식에서 add가 순수함수! add2나 add3는 부수효과가 있어서 순수함수라 할 수 없다. 순수함수는 평가시점에 따라 결과가 달라지지 않고, 항상 동일한 결과를 리턴하기 때문에 안전하다!!

일급함수

함수를 runtime에서 값으로 담을 수 있고, 변수로 담을 수 있고, 인자로 담을 수 있는 것

let f1 = (a) => a*a
console.log(f1(5))

const f3 = (f) => f()
f3(() => 10)

순수함수와 일급함수를 합친 공식을 한번 만들어보자

const add_maker = (a) => {
// 이 부분이 클로저
  return (b) => a+b
}

let add10 = add_maker(10)
console.log(add10(20)) // 30

한 가지 예시 더!

const f4 = (f1,f2,f3) => f3(f1()+f2());
f4(()=>2,()=>1,a=>a*a) // 9

함수형 프로그래밍을 하면 이런 식으로 순수함수를 조합하고, 순수함수들이 최종적으로 결과를 만들면 이를 통해 로직을 만들어간다!

요즘 개발이야기쓰

  • 재미/실시간성 => 라이브 방송, 실시간 댓글, 협업, 메신저
  • 독창성/완성도 => 애니메이션, 무한스크롤, 벽돌
  • 더 많아져야 하는 동시성 => 비동기 I/O, CSP, Actor, STM…
  • 더 빨라야 하는 반응성/고가용성 => ELB, Auto Sacling, OTP Supervisor…
  • 대용량/정확성/병렬성 => MapReduce, Clojure Reducers…
  • 복잡도/MSA… => 많아지고 세밀해지는 도구들… 타협할 수 없는 생산성

스멀스멀 다가오는 FP..(Functional Programming) 좋아지는 하드웨어 성능, 좋아지는 컴파일러로 인해 함수형 프로그래밍 기술도 점점 향상되고 있음!

“함수형 프로그래밍은 애플리케이션, 함수의 구성요소, 더 나아가서 언어 자체를 함수처럼 여기도록 만들고, 이러한 함수 개념을 가장 우선순위에 놓는다.”

“함수형 사고방식은 문제의 해결방법을 동사(함수)들로 구성(조합)하는 것”

ㅡ마이클 포커스 <클로저 프로그래밍의 즐거움> 중…

객체지향에선 데이터를 만든다음에 그 데이터에 맞게 함수를 만드는데, 함수형에선 그 반대로 함수를 먼저 만들고 시작!

우리는 함수형을 왜 써야 하는가보다는 어떻게 전환하는 가에 대해 생각을 해야 한다!

다형성…

method는 객체지향 프로그래밍.
method의 특징은 해당 클래스에 정의되어 있기 때문에 해당 클래스의 인스턴스에만 사용할 수 있음. 예를 들어 map과 filter는 배열에서만 사용 가능쓰

함수형 프로그래밍을 사용하면 다형성도 높아진다!

객체지향은 해당하는 객체가 생겨야 평가를 할 수 있어서 데이터가 꼭 생겨야 하는데, 함수형은 데이터 없이도 함수가 존재할 수 있기 때문에 좀 더 실용적이게 되는 것

image.png

커링

커링은 함수와 인자를 다루는 기법을 말한다.
함수에 인자를 하나씩 적용해나가다가 필요한 인자가 채워지면 함수 본체를 실행하는 것.
JS에선 커링이 지원되지 않지만, 일급함수가 지원되고 평가시점을 마음대로 다룰수 있기 때문에 커링과 같은 기법을 얼마든지 구현할 수 있다.
위에서 만들었던 add_maker도 커링이라고 할 수 있다!

예시 코드를 보자..!

function curry(fn) {
  return function(a) {
    return function(b) {
      return fn(a,b);
    }
  }
}

const add = curry(function(a,b){
  return a+b
})

let add10 = add(10);
add10(5) // 15
add(3)add(5) // 8

fn에 a+b 라는 함수가 들어간것이고, a+b 라는 값을 기억했다가 return 부분에서 a가 인자로 들어가고, b가 인자로 들어가는 함수가 차례로 진행되는데 결론적으로 a+b 더하는 식으로 이어진다.

여기서 add10을 처음에 정의해준 것처럼 변수에 어떠한 값을 먼저 담아야 그게 계속 진행이 되는듯? 어렵다리~~

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

이런식으로도 표현된다고 한다.. 와우…

빼기도 한번 만들어볼건데.. 저 식을 저대로 적용하면 -값 적용이 애매하게 된다. 그래서 순서대로 적용하기 위해 식을 약간 변형해주었다.

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

const sub = curryr(function(a,b) {
  return a-b
})

let sub10 = sub(10);
sub(10)(5) // -5

이렇게 하면 오른쪽에서 왼쪽으로 진행되기 때문에 로직에 잘 들어맞게 된다.

아래는 커링함수를 get함수에 적용한 코드이다. 이렇게 쓰는 방법도 있구나~~~

let users = [
  {id:1, name: 'doori', age: 28},
  {id:2, name: 'dori', age: 38},
  {id:3, name: 'duri', age: 48},
  {id:4, name: 'diri', age: 58},
  {id:5, name: 'dari', age: 18},
  {id:6, name: 'deri', age: 8},
  {id:7, name: 'douri', age: 98},
]

function get(obj, key) {
  return obj === null ? undefined : obj[key]
}

let user1 = users[0];
console.log(user1.name)
console.log(get(user1,'name'))

// 요렇게 하는 방법도 있지만..

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

function get(obj, key) {
  return obj === null ? undefined : obj[key]
}

const _get = curryr(function(obj,key) {
  return obj === null ? undefined : obj[key]})

let user1 = users[0];
console.log(_get(user1, 'name'))
console.log(_get('name')(user1))

여튼 다시.. 정리하자면! 커링이란~! 본체함수를 실행시키기 원하는 시점까지 미뤄두었다가 최종적으로 평가하는 기법이라고 함. 커링 넘 어렵다리~~

reduce

와.. reduce 개념이 엄청 헷갈리고 별로 안 좋아했는데 유인동쌤 최고다 진짜

function _reduce(list, iter,memo) { }

일단 기본 개념은 요로코롬 배열과, 함수와, 직전값이 들어가는건데 아까 위에서 나왔던 add 함수를 생각해보자면…

_reduce([1,2,3], function(a,b) {
  return a+b;
},0)

memo = add(0,1)
memo = add(memo,2)
memo = add(memo,3)
add(add(add(0,1),2),3);

memo는 배열을 돌면서 초기값과 더해주는데 그 값을 기억했다가 직전값과 더해서 최종적인 합계를 도출해낸다.

filter나 map은 배열이 인자로 들어가서 배열을 리턴해내지만, reduce는 배열이 들어가면 어떠한 숫자가 나오거나 하나의 객체를 뱉어낸다!

reduce가 합계를 도출하는건 알았는데 배열이 들어갔을때 객체를 뱉어낸다는건
우리 허선생님을 통해 처음 알게된 사실… 넘 신기 ㅠㅠ
reduce… 앞으로 함수형 프로그래밍을 하려면 자주 쓰게 될것같은데 얼른 더 친해져야겠다 두리듀스!!!! Do Reduce!!!!


그런 의미에서 풀어보는 WeCode JS Exercise 문제!
내가 지난번 면접에서 질문 받았던 것을 예리님께서 문제화 해주셨다.

Q. 4개의 값을 인자로 받는 함수가 있는데, 인자가 null이나 undefined일 경우 프로퍼티에 추가하면 안된다. 예를 들어 input값이 1, null, 3, undefined 이면 output은 { a: 1, c: 3 }이어야 한다.

나의 눈물겨운 첫 시도

const neesArgs = (h,j,k,l) => {
  let obj = {};
  obj = {a:h, b:j, c:k, d:l}
  if (h === undefined || typeof h === null) {
    delete obj.a;
  }
  else if (j === undefined || typeof j === null) {
    delete obj.b;
  }
  else if (k === undefined || typeof k === null) {
    delete obj.c;
  }
  else if (l === undefined || typeof l === ''){
    delete obj.d;
    // null을 어떻게 잡아야하나
  }
  return obj;
}

코드는 굉장히 길고 조잡하지만, null값은 삭제 할 수 없었다.
그러던 중 허선생님의 꿀팁으로 Object.entries()와 reduce를 써보게 되는데…
허선생님이 풀땐 이해가 쏙쏙 된거 같았는데 혼자 하려니 안된다.
그렇다.. 과거의 나는 허선생님의 코드를 완전히 이해를 하지 못한것이었다 ㅋㅋㅋ

const neesArgs  = (...args) => {
  let obj = {a:args[0], b: args[1], c: args[2], d: args[3]};
  return Object.entries(obj).reduce((prev, curr) => {
    const key = curr[0];
    const value = curr[1];
    return (value !== undefined || null) ? Object.assign({},{key : value} ): null
  },'')
}

어떻게든 코드를 줄여보고자 삼항연산자를 썼지만.. 리턴값은 null
접근이 잘못되었다는걸 깨닫고 다른 방법을 찾아본다.

const neesArgs  = (...args) => {
  let obj = {a:args[0], b: args[1], c: args[2], d: args[3]}
  let temp = {}
  let converted = Object.entries(obj).filter(el=> {
    return el[1]
  })
  console.log(converted[0][1],'ㅂㅏ뀐거') // [ [ 'a', 1 ], [ 'c', 2 ] ] 'ㅂㅏ뀐거'
  for(let i in converted) {
    temp[converted[i][0]] = converted[i][1]
  }
  return temp; // { '2': undefined, a: undefined }
}

reduce를 쓰지 않고 for in문을 이용해서 접근해보았다.
이렇게도 값이 나오긴 하는데.. reduce를 너무 쓰고 싶었다. 그래서 이 블로그를 참고해서 내가 짠 식에 적용해보았다.

const neesArgs  = (...args) => {
  let obj = {a:args[0], b: args[1], c: args[2], d: args[3]};
  return Object.entries(obj).reduce((obj, item) => {
    if (item[1] !== undefined && item[1] !==  null) {
      obj[item[0]] = item[1]
    }
    return obj
  },{})
}

결과는 성공적.. 드뎌 제출완료쓰
허선생님은 쫌 더 간단하게 풀었던 것 같은데.. 월요일날 물어봐야지 ㅠㅠ

부족함이 너무 느껴져서 공부할 게 태산인 요즘이지만! 이렇게 하나 하나 개념 정리하면서 넘어가면 언젠간 잘하게 되겠지!!

문어,지지말자!! 화이팅쓰!!! 문어발식 공부하는 모든 개발자분들~

image.png

Reference