본문 바로가기

Study/VanillaJS

[개념잡기] 함수형 프로그래밍 - (4) curry, curryr

[개념잡기] 함수형 프로그래밍 - (4) curry, curryr

서론

함수와 인자를 다루는 기법인 currycurryr 을 구현해보자

함수형 프로그래밍

기본 코드

단순히 인자 두개를 받아서 두개를 더한 값을 리턴하는 함수를 작성해보자

const add = (a, b) => a + b
console.log(add(10, 5)) // 15

curry

curry 함수는 함수의 인자를 한개씩 적용하여 필요한 인자가 모두 채워지면 함수를 실행하는 역할을 수행하며 이로 인해 함수의 평가 시점을 늦출수 있다.

const _curry = (fn) => {
  return (a) => {
    return (b) => {
      return fn(a,b)
    }
  }
}

curry 함수를 이용하여 add 함수를 다시 작성하면

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

var add10 = add(10)
console.log( add10(5) ) // 15
console.log( add10(20) ) // 30
console.log( add(10)(5) ) // 15

위에 add10 함수는 현재 add 함수의 인자 a 에 10 을 넣은 함수를 받은 상태이고 add10(5) 를 통해 b 에 5를 넘겨
(10 + 5) => 10 + 5 의 리턴값인 15 를 출력할 수 있다.

위에 있는 curry 함수를 조금 더 개선해보자. 현재 add 함수는 인자를 각각 한개씩 받는 것은 가능하지만 두개를 동시에 받았을 경우에는 처리하지 못한다.

console.log(add(10)(5)) // 15
console.log(add(10, 5)) // (b) => { return fn(a,b) }

curry 함수에 arguments가 2개라면 인자로 받은 함수를 곧바로 리턴하는 로직을 구현해보자

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

return function(a, b) {} 에서 화살표 함수를 사용하지 못한 이유는 화살표 함수가 arguments 프로퍼티를 지원하지 않기 때문이다.

console.log(add(10)(5)) // 15
console.log(add(10, 5)) // 15

두개 로그 전부 15가 뜨는것을 확인할 수 있다.

그럼 이제 sub 함수를 curry 를 이용하여 작성해보자.

const sub = _curry((a, b) => a - b)

var sub10 = sub(10)

console.log(sub10(5)) // 5
console.log(sub(10, 5)) // 5

console.log(sub(10, 5)) 는 원하는 값이 나왔지만 console.log(sub10(5)) 는 코드상 5에서 10을 뺀 값인 -5가 나와야 하는데 5가 출력되었다.

curryr

curryr 함수를 만들어 오른쪽부터 인자를 처리하도록 바꿔보자.

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

const sub = _curryr((a, b) => a - b)

다시 콘솔에 찍어 보면 원하는 방식으로 로그가 찍힐 것이다.

console.log(sub10(5)) // -5
console.log(sub(10, 5)) // 5

get

오브젝트에 접근해 해당하는 key의 value 를 가져오는 함수를 작성해보겠다

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

console.log( _get(users[0], "name")) // ID
console.log( _get(users[9], "name")) // undefined

여기서 users 는 지난 시간부터 다뤘던 유저의 객체 배열이고, obj 의 존재 여부를 판단하는 방어코드를 작성하여 error 가 발생하지 않게 하였다.

이러한 get 함수에 커링을 적용하면

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

var getName = _get("name")

console.log( _get(users[0], "name")) // ID
console.log(getName(users[0])) // ID
console.log(getName(users[1])) // QW

get 함수를 이용해 만든 getName 이라는 함수는 인자로 객체를 받으면 해당 객체에 있는 name 을 반환한다.

코드 개선

저번에 작성했던 코드 중 30세 이상 유저의 name 을 출력하는 코드가 있었다. get 함수를 이용하여 해당 코드를 개선해 보자

// 개선 전
console.log(
  _map(
      _filter(users, (user) => user.age >= 30 ),
      user => user.name
  )
)

// 개선 후
console.log(
    _map(
        _filter(user, function(user){ return user.age >= 30 }),
        _get("name")
    )
)

정리

지금까지 작성한 함수들이다.

var users = [
  {id: 1, name: 'ID', age: 29},
  {id: 2, name: 'QW', age: 30},
  {id: 3, name: 'WE', age: 31},
  {id: 4, name: 'ER', age: 32},
  {id: 5, name: 'RT', age: 21},
  {id: 6, name: 'DF', age: 28},
  {id: 7, name: 'AS', age: 35},
  {id: 8, name: 'IS', age: 25}
]


const _filter = (list, predi) => {
  var new_list = []
  _each(list, (val) => {
    if(predi(val)) {
      new_list.push(val)
    }
  })
  return new_list
}

const _map = (list, mapper) => {
  var new_list = []
  _each(list, (val) => {
    new_list.push(mapper(val))
  })
  return new_list
}

const _each = (list, iter) => {
  for(var i = 0; i < list.length; i++) {
    iter(list[i])
  }
}

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

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

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

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

const sub = _curryr((a, b) => a - b)

console.log(
  _map(
      _filter(users, function(user){ return user.age >= 30 }),
      _get("name")
  )
)