javascript - 함수형 프로그래밍 (1)

김동하·2020년 9월 30일
0

javascript

목록 보기
39/58

순수함수

오직 return으로 소통하는 함수

const obj1 = {val:10}
function add(obj, b){
  return {val: obj.val + b}
}

add 함수는 인자의 값을 변경하지 않고 외부 상태도 변경하지 않는다. obj1을 참조하면서 새로운 객체를 만들어 리턴한다.

함수형 프로그래밍은 초기값을 유지하면서 값을 다뤄나간다.

일급함수

자바스크립트에서는 함수를 값으로 다룰 수 있다. 함수를 변수에 담을 수 있고, 인자로 넘겨질 수도 있고 모두 다 된다.

예제

_filter, _map으로 리팩토링

함수형 프로그래밍에서는 추상화의 단위가 객체, 메서드가 아니라 함수다. 한 부분을 다른 함수에게 완전히 위임하는 방법을 사용한다. 응용형 함수는 함수를 함수로 받아서 어떤 시점에서 함수를 적용하는 것이다.

1) age가 30세 이하인 사람들

const users = [
    { id: 1, name: "kd", age: 23 },
    { id: 2, name: "df", age: 33 },
    { id: 3, name: "wd", age: 13 },
    { id: 4, name: "as", age: 53 },
    { id: 5, name: "dg", age: 43 },
    { id: 6, name: "gg", age: 33 },
    { id: 7, name: "as", age: 35 },
    { id: 8, name: "hr", age: 43 }
]

function _filter(users, target) {
    const new_list = [];
    for (let i = 0; i < users.length; i++) {
        if (target(users[i])) {
            new_list.push(users[i])
        }
    }
    return new_list
}

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

_filter 함수에 users와 target이란 함수를 인자로 받는다. for 문 안에서 if로 filter역할을 하는 target 함수를 실행시키는데 target의 인자로는 for문이 돌면서 users 배열의 각 객체가 들어간다. return 값은 인자로 받은 user.age >=30 이니까 필터 조건에 맞으면 new_list에 push하고 _filter 함수는 결과적으로 new_list를 뱉는다.

const answer = users.filter(user => {return user.age >= 30})

filter라는 내장 메소드를 쓴 결과와 동일하다.

2) 30세 이상의 유저 name을 수집한다

const over_30 = _filter(users, function(user) { return user.age >= 30 })

function _map(list, mapper) {
    const new_list = [];
    for (let i = 0; i < 0; i++) {
        new_list.push(mapper(list[i]))
    }
    return new_list
}
console.log(_map(over_30, function(user) { return user.name }))

_filter 함수로 걸러진 아이들을 변수에 담고 그 변수를 인자로 준다. for문에서는 _map 함수로 걸러진 친구들만 push한다. mapper 함수는 그냥 받은 배열에서 user.name만 뽑아가면 된다.

const over_30 = users.filter(user => { return user.age >= 30 }).map(el => { return el.name })

내장 메서드를 사용하면 저렇게 작성 가능하다.

_forEach 리팩토링

_map과 _filter 함수에 있는 for문이 중복되므로 for문도 forEach처럼 리팩토링한다.

function _each(list, iterate) {
    for (let i = 0; i < list.length; i++) {
        iterate(list[i])
    }
    return list
}

list와 iterate 함수가 인자다. for문을 돌면서 list의 i번째를 그냥 뱉는다.

function _filter(users, target) {
    const new_list = [];
    _each(users, function(val) {
        if (target(val)) {
            new_list.push(val)
        }
    })
    return new_list
}


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

명령적인 코드보다 선언적인 코드가 많아진다. 간결해진다.

외부 다형성

map, filter 메소드와 함수형 프로그래밍으로 만든 함수와 다른 점은 메소드는 객체 소속이다. 즉, 해당하는 클래스에 준비되지 않으면 사용할 수 없다. 예를들어 유사 배열에 map을 사용할 수 없는 것처럼! 반면 함수형 프로그래밍은 형을 다루기가 쉽다.

커링

커링은 함수와 인자를 다루는 기법이다. 함수에 인자를 하나씩 적용하다가 필요한 인자가 모두 채워지면 함수 본체를 실행한다. 자바스크립트는 커링을 지원하지 않는다. 하지만 일급함수가 있고 평가 시점을 마음대로 지정할 수 있기 때문에 구현이 가능하다.

function _curry(fn){
  return function(a){
    return function(b){
      return fn(a,b)
    }
  }
}

_curry 함수를 실행하면 두 번 리턴하면서 a,b를 모으고 마지막에 원래 인자로 넣어뒀던 fn()에 모았던 인자를 넣고 실행한다.

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

const add10 = add(10)
console.log(add10(5))  // 15

클로저 같은 느낌이다. 만약 add에 인자를 두 개 준다면 함수는 함수를 반환한다. 그래서 이렇게 수정이 가능하다.

function _curry(fn) {
    return function(a, b) {
        if (arguments.length === 2) {
            return fn(a, b)
        }
        return function(b) {
            return fn(a, b)
        }
    }
}

이럴 거면 처음부터 저렇게 쓰지... 라는 생각이 들지만 아무튼 훌륭하다.

get

get은 객체에 있는 값을 안전하게 참조하는 데 사용된다.

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

이 get을 curryr (인자를 오른쪽부터 적용하는 함수)에 적용시켜서 함수 return 부분을 더 간편하게 만들 수 있는데 여기까지는 못하겠다. 일단 get은 pass!

reduce

_each, _curry, 커리에 담긴 add를 이용하여 _reduce를 만들 수 있다.

function _each(list, iterate) {
    for (let i = 0; i < list.length; i++) {
        iterate(list[i])
    }
    return list
}

function _curry(fn){
  return function(a,b){
    if(arguments.length === 2){
      return fn(a,b)
    }
    return function(b){
      return fn(a,b)
    }
  }
}

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

위 3개의 함수를 이용하여

function _reduce(list, iterate, memo) {
    _each(list, function(val) {
        memo = iterate(memo, val)
    })
    return memo
}

console.log(_reduce([1,2,3,4], add, 0)) // 10

먼저 _reduce 함수 첫 번재 인자로 배열을 받는다. 그리고 두 번째로 add, 마지막으로 초기값인 0을 받는다.
구조를 살펴보면 _each를 통해 list를 받아 하나씩 꺼낸다. _each의 두 번째 인자에

function(val) {
        memo = iterate(memo, val)
    }

요게 들어간다. 즉, val = list[i]다. _each 함수 안에서 for문을 돌면서 memo값을 갱신(혹은 덮어쓴다.)

for문 1회차 때 list[0] = 1이고 memo는 0이다. _reduce 함수 안에서

iterate(0, 1)

이렇게 되고 커리된 add는 두 인자를 더한다. 더한 값이 이제 memo가 되는 것이다. 계속 반복해서 마지막 memo를 리턴하면 reduce 메서드를 구현한 값이다. 함수가 겹치니까 굉장히 헷갈린다..

_reduce 함수를 이용해서 파이프 함수 만들기

파이프는 함수들을 인자로 받아서 함수들을 연속적으로 실행한다. 즉, 함수를 리턴하는 함수다. 파이프는 결국 reduce인데 파이프에서 좀 더 추상화되면 reduce가 된다.

function _pipe(){}

const f1 = _pipe(
    function(a) { return a + 1 },
    function(a) { return a * 2 }
)

f1(1)

_pipe에 인자를 1로 주면 첫 번째 함수에서 2가 되고 2가 두 번째 함수로 가면서 4가 된다. 이런 식으로 함수를 함수로 주는 것이 파이프! _reduce와 결합하여 _pipe를 조져보자.

function _pipe() {
    const fns = arguments;
    return function(arg) {
        return _reduce(fns, function(arg, fn) {
            return fn(arg)
        }, arg);
    }
}

const f1 = _pipe(
    function(a) { return a + 1 },
    function(a) { return a * 2 }
)

console.log(f1(1))

?

너무 피곤해서 일단 자고 다시 시작해본다. 일단 arguments로 여러개의 함수를 받고 함수를 리턴한다. 그 함수는 다시 _reduce를 리턴한다. _reduce는 클로저로 만들었던 fns와 f1()에서 인자로 들어갈 값을 arg로 준다. 그 인자는 _reduce의 시작값이다. (실행 과정은 이해가 안 감)

출처 : https://www.inflearn.com/course/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D/lecture/6784?speed=1.25&tab=note

참고 : https://www.inflearn.com/course/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D/lecture/6772?speed=1.5&tab=note

profile
프론트엔드 개발

0개의 댓글