함수형 프로그래밍과 javascript es5를 활용한 구현

함수형 프로그래밍

  • 함수형 프로그래밍(函數型 프로그래밍, 영어: functional programming)은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. 명령형 프로그래밍에서는 상태를 바꾸는 것을 강조하는 것과는 달리, 함수형 프로그래밍은 함수의 응용을 강조한다. (출처 : 위키피디아)
  • 함수형 프로그래밍은 함수의 조합을 통해 로직을 구현하는 것에 있다.
  • 함수형 프로그래밍은 순수함수의 구현을 목표로 한다. 순수함수는 언제 어디서든 하나의 함수에 동일한 값을 넣으면 동일한 결과를 반환한다.
  • 순수함수를 구현하기 위해서는 함수형 프로그래밍은 불변성을 유지하고 부수효과(side effect)를 제거한다.
    • 삽입된 인자는 변경되면 안된다. 내부에서 별도의 복사한 데이터를 생성하고 결과값을 리턴한다.
    • 인자가 처리되는 과정에서 함수 외부에 영향을 미쳐서는 안된다. 부수 효과가 없어야 한다.
    • 인자가 처리되는 과정에서 함수 내부에 데이터가 남아서는 안된다. 멱등성을 보장해야 한다.

자바스크립트와 일급함수

  • 함수 간 조합이란 함수가 일급임을 보장될 때 가능하다. 자바스크립트의 함수는 일급이다. 일급이란 다음과 같은 의미와 특징을 가진다.

함수를 변수에 담을 수 있다.

var f1 = function(a){return a * a;};
var f2 = add;

함수가 함수를 인자로 받을 수 있다.

function f3(f) {
    return f();
}

f3(function(){return 10;}); // 10

함수가 값을 보관하고 있다

  • 아래의 형태를 클로저라 한다.
function add_maker(a){
    return function(b){
        return a + b;
    }
}

let add30 = add_maker(30); 
add30(20); // 50

함수형 프로그래밍 구현하기

  • 함수의 이름은 관행적으로 언더바로 시작한다.
    • _filter(), _map()
  • 함수형 프로그래밍의 구현 예제는 아래와 같다.
  • 모든 예제는 html을 기반으로 작성되어 있다.
  • 구현된 함수는 기본적으로 인자를 배열(es6 이후는 iterator)로 받는다.

filter, map, each

  • 함수형 프로그래밍의 매력은 작은 함수의 조합으로 큰 함수가 구현됨에 있다.
  • 아래의 예제는 함수형 프로그래밍의 가장 기초적인 함수로서 각각 filter, map, each이다.
  • each는 일급함수와 배열을 인자로 한다. 배열을 순회함에 있어 인자로 삽입된 일급함수의 로직에 따라 처리한다. each는 함수형 프로그래밍에 있어서 가장 기본적인 함수이다. 왜냐하면 filter와 map이 each로부터 시작하기 때문이다.
  • filter는 배열 중 특정 상태인 값만 추려서 배열로 반환하는 함수이다.
  • map은 배열의 각 값을 다른 형태로 변환하여 반환하는 함수이다.
  • 함수형 프로그래밍의 매력은 이미 시작되었다. each라는 작은 함수가 filter와 map으로 확장된다. 함수의 조합을 통한 프로그래밍의 가치를 여기서부터 느낄 수 있다.
<script>
    // log를 간단하게 작성하기 위하여 변수를 정의한다. 
    const log = console.log;
</script>

<script>
    // given
    const products = [
        {name: '반팔티', price: 15000, stock: 10},
        {name: '긴팔티', price: 20000, stock: 5},
        {name: '핸드폰케이스', price: 15000, stock: 154},
        {name: '후드티', price: 30000, stock: 1},
        {name: '바지', price: 25000, stock: 999}
    ];
</script>

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

    // each 의 활용
    function _filter(list, predi){
        let new_list = [];
        _each(list, (v) => predi(v) ? new_list.push(v) : null);
        return new_list;
    }

    let resultFilter = _filter(products, (p) => (p.price>=30000));
    log(resultFilter[0].name=='후드티');

    function _map(list, mapper){
        let new_list = [];
        for (let i = 0; i <list.length; i++) {
            new_list.push(mapper(list[i]));
        }
        return new_list;
    }

    // each 의 활용
    function _map(list, mapper){
        let new_list = [];
        _each(list, v => new_list.push(mapper(v)));
        return new_list;
    }

    let resultMap = _map(products, (p) => p.name);
    log(resultMap[0] == "반팔티");
    log(resultMap[4] == "바지");
</script>

curry

  • 함수형 프로그래밍은 완성된 함수에 인자를 삽입한다. curry는 함수의 조합을 가능하게 하는 함수이다. 함수를 먼저 정의하고 평가(계산)를 위한 인자를 나중에 삽입하도록 미룬다.
  • curryr은 curry로부터 파생된 함수이다. 인자의 순서를 반대로 배치한다. 직관적으로 읽기 편하도록 인자 입력 순서를 반대로 한다.
<script>
    function _curry(fn){
        return function(a, b){
            // 인자가 하나일 경우 클로저를 사용하여 평가를 미루도록 한다. 
            return arguments.length == 2 ? fn(a, b) : function(b){ 
                return fn(a, b);
            }
        }
    }

    // 즉각 평가한다. 
    let sub = _curry((a, b)=>(a-b));
    log(sub(10, 20) == -10);

    // 지연 평가한다.
    let sub10 = sub(10);
    log(sub10(20) == -10);

    function _curryr(fn){
        return function(a, b){
            return arguments.length == 2 ? fn(a, b) : function(b){
                return fn(b, a);
            }
        }
    }
    let subr = _curryr((a, b)=>(a-b));
    log(subr(10, 20) == -10);
    let subr10 = subr(10);
    log(subr10(20) == 10);  // 삽입된 인자의 10을 뺀 값을 리턴한다. 의미가 더 분명하다.
</script>

get

  • 객체에서 특정 프로퍼티를 찾는다.
  • curry를 사용하여 지연평가할 수 있다.
<script>
    const _get = _curryr(
        (obj, key) => (obj == null) ? undefined : obj[key]
    );

    let getStock = _get('stock'); // curry를 활용하여 지연평가한다.
    log(getStock(products[0]) == 10);
    log(getStock({name : 'kim'}) == undefined);
</script>

map, filter with curry

  • curry를 활용하여 map과 filter를 재정의한다.
  • map과 filter의 인자를 마지막에 넣어, 평가를 늦춘다.
<script>
    _map = _curryr(_map);
    _filter = _curryr(_filter);

    let mapByStock = _map(_get('stock'));
    log(mapByStock(products)[0]== 10);
    log(mapByStock(products)[4]== 999);

    let priceGreaterThan20000 = _filter((x) => x.price > 20000);
    log(priceGreaterThan20000(products)[0].name=='후드티');
    log(priceGreaterThan20000(products)[1].name=='바지');
</script>

reduce

  • 배열을 하나의 값으로 리턴한다. 총합이나 평균에 사용한다.
  • reduce를 구현하는 다양한 방법이 있으며 아래는 그것을 나열하였다.
<script>
    function _reduce(items, iter, memo){
        for (let i = 0; i < items.length; i++) {
            memo = iter(memo, items[i]);
        }
        return memo;
    };

    function _reduce(items, iter, memo){
        _each(items,
            (item) => (memo = iter(memo, item))
        );
        return memo;
    };

    function _rest(items, num = 1) {
        return Array.prototype.slice.call(items, num);
    }

    function _reduce(items, iter, memo){
        if(arguments.length==2){
            memo = items[0];
            items = _rest(items);
        }
        _each(items,
            (item) => (memo = iter(memo, item))
        );
        return memo;
    };

    log(_reduce([1,2,3], (a, b) => a + b, 5) == 11) // 5부터 시작한다. ((5 + 1) + 2) + 3 = 11;
    log(_reduce([1,2,3], (a, b) => a + b) == 6) // 1부터 시작하며 리스트는 2 개이다.  (1+2)+3 = 6;
</script>

pipe

  • 일급함수는 배열에 넣을 수 있다.
  • reduce는 숫자 등 배열의 값을 특정 로직으로 처리하여 하나의 값으로 리턴한다.
  • 일급함수의 배열을 reduce로 처리할 수 있다. 여러 개의 일급함수를 하나의 함수로 조합하여 리턴한다. 완성된 함수에 인자를 넣을 수 있도록 구현한다.
<script>
    let fns = [
        (a) => a + 2,
        (a) => a * 2
    ];
    // 함수의 배열을 리스트로 넣고, 삽입된 인자(memo)를 해당 함수의 값으로 반복하도록 구현한다.
    log(_reduce(fns, (memo, fn) => fn(memo), 1) == 6); // (1 + 2) * 2 = 6


    function _pipe(){
        const fns = arguments;
        return function(x){
            return _reduce(fns, (memo, fn) => fn(memo), x);
        }
    }

    let pipeResult = _pipe(
        (a) => a + 2,
        (a) => a * 2);
    log(pipeResult(1) == 6); // (1+2)*2 = 6
</script>

go

  • pipe는 함수를 정의한 후 인자를 삽입한다. go는 인자를 가장 먼저 삽입한다.
  • 인자를 가장 먼저 배치하고 그것이 처리되는 함수를 차례대로 나열한다. 직관적이고 이해하기 쉽다.
  • 함수의 스트림이 계속 연결되기 때문에 순수 함수이자 부가 효과가 없는 좋은 함수를 구현할 수 있음.
<script>
    function _go(){
        return _pipe.apply(null, _rest(arguments))(arguments[0]);
    }

    let goResult = _go(
        1,
        (a) => a + 2,
        (a) => a * 2);
    log(goResult==6);

    // go를 활용하여 map과 filter를 적극적으로 활용 가능하다.
    let goResult2 = _go(
        products,
        _filter(p => p.price>20000),
        _map(p => p.name)
    );
    log(goResult2[0] == "후드티");
    log(goResult2[1] == "바지");
</script>

에러에 대응

  • 함수형 프로그래밍은 에러 및 문제 상황에 대하여 throw Exception 으로 로직을 멈추거나 내부에서 catch 하지 않는다. null 에 대한 에러를 undefined로 치환하거나 그냥 빈 값을 전달하는 등 문제 상황을 엄격하게 관리하지 않는다. 클라이언트가 결과를 확인하고 정상 값으로 수정하도록 유도한다.
  • each를 에러에 대응하도록 다음과 같은 기준에 따라 개선한다.
    • null 혹은 빈 값을 인자로 할 경우 빈 배열을 반환한다.
    • array-like의 경우 for..i가 동작하지 않는다. 그러므로 앞서 작성한 each 함수가 정상 수행되지 않는다. Object.keys를 활용하여 array-like 문제를 해소한다.
<script>
    function _is_object(obj){
        return typeof obj == 'object' && !!obj;
    }

    log(_is_object(null) == false);
    let undefinedVar;
    log(_is_object(undefinedVar) == false);
    log(_is_object({abc:123}) == true);

    // array-like에 대응한다
    function _keys(obj){
        return _is_object(obj)?Object.keys(obj):[];
    }

    log(_keys(null).length==0);
    log(_keys(undefinedVar).length==0);
    log(_keys({abc:123}).length==1);
    log(_keys(document.querySelectorAll('*')).length>0);
    log(_keys({abc:123})[0]=='abc');
    log(_keys([0,1,2,3])[0]=='0'); // array는 index를 리턴한다.

    // 기존 형태
    function _each(list, iter){
        for (let i = 0; i < list.length; i++) {
            iter(list[i]);
        }
    }

    // each를 발전된 형태로 작성한다.
    function _each(list, iter){
        let keys = _keys(list);
        for (let i = 0; i < keys.length; i++) {
            iter(list[keys[i]]); // keys를 기준으로 values를 꺼낸다.
        }
    }

    // array like가 정상 동작한다.
    let eachList = [];
    _each({
        13:'ID',
        18:'HD',
        29:'YD'
    }, function(v){
        eachList.push(v);
    });
    log(eachList[0] == 'ID');
    log(eachList[1] == 'HD');
    log(eachList[2] == 'YD');

    // each를 사용하는 다른 함수 역시 정상 동작한다.
    let goResult3 = _go(
        products,
        _filter(p => p.price>20000),
        _map(p => p.name)
    );
    log(goResult3[0] == "후드티");
    log(goResult3[1] == "바지");
</script>

나아가며

  • 자바스크립트로 함수형 프로그래밍에 사용하는 함수를 하나씩 구현해봤다. 함수형 프로그래밍의 매력을 충분히 맛볼 수 있었다. 자바를 주로 사용하는 입장에서 객체지향 개발이 최선의 개발이라 생각했던 짧은 시야를 깨뜨릴 수 있었다.
  • 객체 내부의 캡슐화와 불변성을 지키기 위한 장황한 코드는 자바 개발자라면 익숙할 것이다. 단순한 getter로도 데이터에 오염이 발생할 여지가 있는 필드는, 값을 받을 때 불변으로 만들고 리턴할 때는 새로운 객체로 복사하여 전달하는 것은 방어적인 코드는 익숙하다. 이것 외에도 자바는 코드를 장황하게 만드는 경향이 있다. 더 나아가 객체 내부의 필드와 메서드는 의미를 지녀야 하므로 이름 짓기를 강요 받는다. 객체 또한 복잡하여 VO와 DTO, request와 response 따위의 접미사 가진 클래스 수십여개가 생성된다.
  • 함수형 프로그래밍의 특징은 이러한 객체 구현을 최소화 한다. 이러한 특징을 가장 잘 드러내는 것이 _go 함수이다. 함수형 프로그래밍이 매력이 폭발하는 부분은 _go 라 생각한다. 첫 번째 인자로 데이터를 명시한다. 나머지 인자로 해당 데이터를 처리하기 위하여 작은 함수를 조합한다. 이러한 흐름 속에서 외부에 영향을 주거나 받지 않는 순수함수로서 동작한다. 매우 단순하고 명확하게 코드를 작성한다. 인터페이스가 아닌 컬렉션을 인자로 하는 함수형 프로그래밍의 특징이 잘 드러난다. 엄격한 데이터 타입과 클래스 구현보다 확장성에 방점을 찍는다.

다음의 수업을 학습하고 블로그를 정리하였습니다. 매우 강추 합니다! : 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/