[JS] 함수형 프로그래밍

yejineee·2020년 12월 11일
0

자료 처리를 수학적 함수의 계산으로 취급하고,

상태와 가변 데이터 대신 불변 데이터를 사용하는 프로그래밍

로직을 반복하며, 값들은 복사됨

추상화의 단위는 함수.

순수 함수

  • 매개 변수 외에 다른 변수에 접근하지 않는다
var z = 10;
function add(x, y) {
    return x + y;
}

x, y외에 z에 접근하지 않는다.

  • 유용한 순수 함수는 한 개 이상의 매개변수를 갖는다.

  • 유용한 순수 함수는 반드시 반환값이 있어야 한다.

  • 느긋한 계산 Lazy Evaluation

    : 항상 같은 값이 나오기 때문에, 계산을 느긋하게 해도 됨.

  • 참조 투명성

    : 다른 것을 참조하여도 그 값이 변화하지 않음.

  • 변경 불가능한 데이터

  • 순수 함수는 같은 입력 값을 넣었을 때 항상 같은 출력값을 반환한다.

  • 순수 함수는 부작용이 없다.

    변수 값은 프로그램 어디에서든 변할 수 있으므로, 디버깅이 힘들어진다. 이러한 부작용을 없애는 것이 순수 프로그램을 작성하는 핵심이다.

불변성

불변하는 것이 좋은 이유는 프로그램 내 변수가 실수로 값이 바뀔 일이 없는 것이다.

프로그램이 멀티 쓰레드 기반이면, 다른 쓰레드가 그 값을 수정할 수 없다.

  • 함수형 프로그래밍에서는 변수가 없다.

    저장된 값은 변수라고 불리긴 한다. 그러나 어떠한 변수에 값이 할당되면 그 값은 계속 유지된다. 즉, 어떠한 변수에 대해서 읽기 권한만 가진다.

    함수형 프로그래밍에서 값을 변하게 하려면, 값을 복사해서 변하게 한다.

  • 함수형 프로그래밍에서 반복을 하려면 재귀 함수를 사용한다.

    반복문은 변수가 변하는 것을 요구하므로, 좋지 않다.

    재귀를 사용하면 기존 값을 변경하지 않는다.

    // simple loop construct
    var acc = 0;
    for (var i = 1; i <= 10; ++i)
        acc += i;
    console.log(acc); // prints 55
    // without loop construct or variables (recursion)
    function sumRange(start, end, acc) {
        if (start > end)
            return acc;
        return sumRange(start + 1, end, acc + start)
    }
    console.log(sumRange(1, 10, 0)); // prints 55
  • 자바스크립트에서의 불변성

    • const 로 한 번 변수가 설정되면 다시 설정할 수 없게 만들 수 있따.

      const a = 1;
      a = 2; // 크롬, 파이어폭스, 노드에서 TypeError를 던진다 
             // 하지만 사파리는 잘된다 (circa 10/2016)
    • 람다

      const f = R.curry((a, b, c, d) => a + b + c + d);
      console.log(f(1, 2, 3, 4)); // 10 출력
      console.log(f(1, 2)(3, 4)); // 역시 10 출력 
      console.log(f(1)(2)(3, 4)); // 역시 10 출력

Lazy Evaluation

  • 적극적 계산 ( eager evaluation) 에서는 함수 호출 전에 모든 인자 목록을 계산해 놓음.

  • 느긋한 계산 ( lazy evaluation) 에서는 실제 함수 호출 시에 그 값이 필요할 때까지 계산하지 않음

  • 장점 : 메모리 이득!!!

    리턴 타입으로 리스트가 넘어가더라도 계산되지 않고, Reference만 넘어가게 됨.

    그 값을 실제로 참조할 때 계산이 됨으로, 메모리를 적게 사용하게 됨.

리팩토링

중복 사용하는 여러 함수를 하나의 함수로 줄이는 것이다.

function validateSsn(ssn) {
    if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn))
        console.log('Valid SSN');
    else
        console.log('Invalid SSN');
}
function validatePhone(phone) {
    if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone))
        console.log('Valid Phone Number');
    else
        console.log('Invalid Phone Number');
}

같은 정규식을 사용하는 두 함수를 아래의 함수로 하나로 줄일 수 있다.

function validateValue(value, regex, type) {
    if (regex.exec(value))
        console.log('Invalid ' + type);
    else
        console.log('Valid ' + type);
}

고차함수

  • 함수는 단순한 값이기 때문에 매개변수로 전달할 수 있다.
function validateAddress(address) {
    if (parseAddress(address))
        console.log('Valid Address');
    else
        console.log('Invalid Address');
}
function validateName(name) {
    if (parseFullName(name))
        console.log('Valid Name');
    else
        console.log('Invalid Name');
}

위의 두 함수는 서로 다른 정규식을 확인해야 한다. 이 정규식을 확인하는 함수를 매개변수로 넘겨주면, 리팩토링이 가능해진다.

function validateValueWithFunc(value, parseFunc, type) {
    if (parseFunc(value))
        console.log('Invalid ' + type);
    else
        console.log('Valid ' + type);
}

위 함수는 parseFunc라는 함수를 매개변수로 전달받아서 실행시키게 된다.

이러한 함수를 고차함수라고 한다.

위 4개의 함수를 고차함수로 호출할 수 있다.

function makeRegexParser(regex) {
    return regex.exec;
}

var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);
var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/);

validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

Regax.exec는 정규식에서 일치하는 문자열이 있으면 그 문자열을 반환해준다.makeRegexParser함수는 exec함수를 반환하는 고차함수이다.

이제 고차함수는 함수를 매개변수로 전달받을 수도 있고, 함수를 반환할 수 있다는 것을 확인해보았다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cd7677e3-0da6-418a-a8e0-d849d55b4325/_2020-08-07__11.58.27.png

map

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

매개변수 f는 array에 있는 요소에 원하는 작업을 하나씩 할 수 있게 해준다.

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

map 함수 내부적으로 for-loop이 동작한다. 하지만 최소한 boilerplate 코드를 반복할 필요가 없어졌다.

filter

arr.filter(callback(element[, index[, array]])[, thisArg])
var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

reduce

리스트를 입력으로 받아서 하나의 값으로 줄이는데 사용된다.

var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15

클로저

  • 클로저라는 것은 함수에 대한 참조로 인해 계속 살아있는 함수의 스코프다.
function grandParent(g1, g2) {
    var g3 = 3;
    return function parent(p1, p2) {
        var p3 = 33;
        return function child(c1, c2) {
            var c3 = 333;
            return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;
        };
    };
}

이 예제에서 child는 자신의 변수와 parent의 변수, 그리고 grandParent의 변수에 접근한다.

그리고 parent는 자신의 변수와 grandParent의 변수에 접근한다.

grandParent는 오직 자신의 변수에만 접근할 수 있다.

var parentFunc = grandParent(1, 2); // returns parent()
var childFunc = parentFunc(11, 22); // returns child()
console.log(childFunc(111, 222)); // prints 738
// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738

grandParent가 parent를 반환하는 시점부터 parentFunc는 parent의 스코프를 보관한다.

마찬가지로 parentFunc가 child를 반환하는 시점부터 childFunc는 child의 스코프를 보관한다.

함수가 생성될 때, 함수의 스코프 내에 존재하는 모든 변수는 함수의 생명 기간(lifetime)에 접근이 가능하다. 함수는 참조가 남아있는 한 계속 존재한다. 예를 들어, child의 스코프는 childFunc가 계속 참조하는 한 존재한다.

add10을 한 후에 그 결과가 mult5로 흘러들어가게 된다.

Point-Free Notation

  • Point-Free 표기법은 함수를 작성할 때, 매개변수를 정의하지 않는다.

    -- 1개의 매개변수를 기대하는 함수
    mult5AfterAdd10 value =
        (mult5 << add10) value

    여기서 value는 2번 명시가 되었다. 그러나 여기서 add10이 같은 매개변수를 기대하므로, 매개변수는 필요하지 않는다.

    -- 마찬가지로 1개의 매개변수를 기대하는 함수
    mult5AfterAdd10 =
        (mult5 << add10)

    이렇게 고칠 수 있다.

  • 장점

    • 불필요한 매개변수를 사용하지 않아도 된다. 그렇기 때문에 그것들에 대한 이름을 생각하지 않아도 된다.
    • 훨씬 간결해지기 때문에 읽기에도 편하다. 이 예제는 간단하지만, 매개변수가 엄청 많은 함수를 상상해보자.

합성 함수

  • 구체적인 기능을 하는 함수들을 조합하여 합성함수를 만든다.
add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value
  • f(g(h(x))) 와 같은 표현들을 읽기 어렵다고 한다. 이런 경우는 함수 합성을 통해 풀어서 만든다.

    pipe operator에 값을 연속으로 넣는다.

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0bd4969b-c5c6-477e-a81e-48d79fd501b0/_2020-08-08__2.18.02.png

Pipe

함수들을 인자로 받아서, 함수를 연속적으로 실행하는 함수를 리턴한다.

const pipe = (...funcs) => v => {
  return funcs.reduce((res, func) => {
    return func(res);
  }, v);
};

const pipe = (...fns) => (v) => fns.reduce((arg, fn) => fn(arg), v);

클로저를 이용하여 fns를 참조하게 된다. 파이프는 v라는 값을 받는 또 다른 함수를 반환하게 된다.

이 함수가 v를 받을 때까지 파이프는 reduce를 실행시키지 않는다. 이는 lazy evalution을 가능하게 한다.

함수형 프로그래밍 - Pipe

커링

  • 커링 함수는 한 번에 단 한 개의 매개변수만 받는 함수이다.
  • 함수에 인자를 하나씩 적용하다가 필요한 인자가 모두 채워지면, 함수 본체를 실행하는 기법.
function _curry(fn){
    return function(a){
        return function(b){
            return fn(a,b) ; 
        }
    }
}

const add = _curry((a, b) => a+b) ; 
const add10 = add(10) ; 
console.log(add(5)(3)) ; // 8
console.log(add10(100)) ; // 110
console.log(add(1,2)) ; // [Function]

add(1,2)처럼 인자가 2개가 들어오면, function(b)가 return이 된다.

인자가 2개가 들어와도 함수가 실행되도록 바꾸면 더 유용하다.

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

const add = _curry((a, b) => a+b) ; 
const add10 = add(10) ; 
console.log(add(5)(3)) ;  // 8
console.log(add10(100)) ; // 110 
console.log(add(1,2)) ;   // 3

오른쪽에서부터 인자를 적용하는 커리함수

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

const sub = _curryr((a,b) => a-b) ;
const sub10 = sub(10) ; 
console.log(sub10(5)) ; // -5

0개의 댓글