함수형 프로그래밍 슬쩍보기

수박·2021년 8월 13일
0
post-thumbnail

Side effect가 없이 in, output이 동일한 순수함수로만 작성되고 공유상태, mutable을 피하는 SW를 만드는 프로세스

함수형 프로그래밍은 명령형(imperative) 이 아닌 선언형(declarative) 이며 애플리케이션의 상태는 순수 함수를 통해 전달됩니다. 애플리케이션의 상태가 일반적으로 공유되고 객체의 메서드와 함께 배치되는 객체 지향 프로그래밍과는 대조됩니다. 출처

함수형 프로그래밍은 딱 정해진 것이 아니라, 특징들을 갖는 프로그래밍의 패러다임이다.

애플리케이션의 상태가 일반적으로 공유되고 객체의 메서드와 함께 배치되는 객체 지향 프로그래밍과는 대조된다고 한다.

나는 애플리케이션의 상태(클래스의 멤버변수)가 공유되고 메서드를 통해 처리되는 객체지향과 선언형이 다르다고 이해했다.

그렇다면 함수형에서는 객체지향에서 공유하는 상태(메소드, 멤버변수)를 어떻게 다루며,

이 상태를 전달하는 순수함수란 무엇인지 알아보고, 나머지 특징에 대해서도 알아보자

함수형 프로그래밍의 특징

함수형 프로그래밍은 다음 사항들을 지키는 프로그래밍 방법이다.

  • 순수함수
  • 합성함수
  • 공유상태를 피해라
  • 상태 변화를 피해라
  • 부작용을 피해라
    • Side effect란 함수의 실행으로 인한 외부의 변화나 메모리, I/O 적에서 영향을 뜻한다.

순수함수 ?

  1. 같은 input은 항상 같은 output
  2. side effect가 없는 함수

순수함수는 절대로 변수를 변경하지 않고 오직 새로운 하나의 결과만을 만드는 함수다. input에 따라 항상 동일한 output을 내며 side effect가 없는 함수를 순수함수라고 일컫는다.

전역변수에 의존적인 함수는 다른 곳에서 전역변수가 변경될 여지가 있으므로 output이 동일하지 않아 그에 따른 side effect가 생길 수 있다.

이런 side effect가 없는 함수들로 프로그래밍을 해 가독성을 높이고 유지보수를 용이하게 하는 프로그래밍 방법이 함수형 프로그래밍이다.


같은 input, 항상 같은 output

처음 배울 때 함수는 어떤 걸 박스에 넣으면 박스 바깥으로 뭐가 나온다라고 많이 배운다.

이미지 출처

이처럼 input을 넣으면 항상 y라는 output이 나오는 것이 함수의 기본 개념이다.

이 함수를 구현하는 방법이 나뉘게 되는데 프로그래밍의 패러다임에 따라 갈린다.

패러다임에는 명령형, 선언형, 객체지향형이라는 등등의 관점이 존재한다.

먼저, 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 가깝고,

선언형은 무엇을 할 것인가와 가깝다.

배열에서 짝수인 숫자를 얻는 방법을 예로 들어보자.

하나는 명령형 프로그래밍 방법으로, 하나는 선언형 프로그래밍 방법으로 보자보자


명령형 방법

  • 10개의 숫자가 담긴 배열에서 처음부터 끝까지 반복해서 돌며 각각의 숫자가 짝수인지 확인한다.

선언형 방법

  • 짝수인 숫자

보다시피 명령형은 절차적이다. 원하는 값을 얻기 위해 뭐부터하고 그다음에 뭘 할지를 기술한다.

선언형은 우리가 만들어낼 무엇 == 짝수에 집중된다.

코드로 어떻게 구현할까?

for문 돌려서 조건해서 뽑아내기

그게 명령형이라고 불리는 방법이다.

그렇다면 선언형은 ?

코드로 예시를 들어보자.

// 명령형
let numbers = [1,2,3,4,5,6,7,8,9,10]
let 짝수배열 = [];
for (let i = 0; i < numbers.length; i++){
  if (numbers[i] %= 2 == 0)
      짝수배열.push(numbers[i]);
}
return 짝수배열;
//선언형
let 짝수배열 = numbers.filter((number) => number %2 == 0);

명령형코드는 for가 몇부터 몇까지 도는지, if문의 조건이 무엇인지. 그때 수행되는 코드가 무엇인지 마지막으로 반환되는 값이 무엇인지 까지 확인해야 해당 함수를 이해할 수 있다.

반면, 선언형은 filter라는 함수가 어떤 기능을 하는지(콜백의 조건에 해당하는 값만 반환)만 알면 코드를 이해할 수 있다.

엄청 간결해져서 이해하기도 편하다.

왜 갑자기 이 얘길하냐면 현재 다루고 있는 함수형 프로그래밍이 선언형에 속해있기 때문이다.

명령형 프로그래밍은 프로그래밍 순서에 따라 이전 상태(i, result)를 바꿔가며 계산해가는 패러다임인 반면 함수형은 어떤 값(numbers)을 주고 함수를 실행해 반환된 결과값을 가지고 함수를 연쇄적으로 호출해서 원하는 결과값을 뽑는다라는 차이점이 있다.

여기서 상태의 전달에 대해서 함수형 프로그래밍에서는 함수에 상태(input)를 전달해 output을 얻는 과정이 항상 동일해야한다는 것을 이해할 수 있다.

항상 동일한 input에 대한 동일 output을 반환하는 것이 순수함수의 특징이라고 했다.

// 순수함수
function add (a, b) {return a + b};
// 순수함수 X
function rand() {return Math.random() }

매번 동일한 결과를 뱉는 add와 달리 rand는 결과가 다르므로 순수함수가 아니다.

좀 돌아갔는데,, 결론은 프로그래밍의 단위인 함수를 구현하는데에 있어 방법이 나뉜다는 것이다.

여기서 함수형 프로그래밍 방법을 하기 위해선 선언형과 같이 작성해야한다는 것이고 add와 같이 동일 인풋, 동일 아웃풋을 보장하는 순수함수로 이루어지는 프로그래밍을 해야한다는 것이다.


side effect가 없는 순수함수

순수함수라고 일컫는 함수는 동일 input, output과 동시에 side effect가 없어야한다.

외부변수에 대한 접근 유무로 쉽게 이해할 수 있다.

예시를 들면 두 수를 합하는 sum이라는 함수가 있다.

function sum (a, b) {
    return a + b
}

이 함수에 항상 1, 1를 넣었을 때 항상 같은 결과인 2가 나온다

1 + 1은 2니까, 항상 2가 나올 것이다.

그 이외의 어떠한 동작을 하지 않고 오직 두 수를 더한 결과만을 반환한다.

100, 1를 넣든 10, 20을 넣든 항상 두 수를 더한 결과만을 반환한다.

동일한 기능을 하지만 side effect가 있는 함수의 예시를 보자

let gv = 2;
function sum (a, b) {
    gv = a;
    return a + b;
}

이 함수도 1, 1을 넣든, 100, 1을 넣든 두 수를 합한 결과만을 반환하지만, 전역변수를 변경하는 기능도 있다.

이게 sum함수의 side effect이며 해당 함수는 순수함수가 아니다.

순수함수가 따로 있는 것이 아닌 순수함수의 특징을 지키면 순수함수가 되는 것이다.

그러나 모두 다 순수함수로만 될 수 없을 것이다. 로직을 구성하는데에 있어서 외부변수를 조작하게되는 경우가 생겼을 때 그 부분도 함수단위로 나누어 side effect를 줄이는 것을 목표로 해야한다.

함수형 프로그래밍은 순수함수로 이루어져있고 이런식으로 구현하면 되는건가 ? -> ㄴㄴ;!! 이외의 여러 특징이 존재함 ! 순수함수로 이루어져있어서 외부 상태를 변경하는 부작용이 없고 추상화되어 있어서 간결해서 이해하거나 유지보수나 테스트가 쉬움!! 그리고 함수들을 조합할수도 있음!

~간결하다는 것 까지는 이해했는데, 함수 조합에 대해 알기전에 먼저 왜 조합해서 사용하는지, 어떻게 조합할 수 있는 것인지에 대해 알아봐야한다.~


공유상태를 피하자

동일 scope내에 있는 변수, 객체 또는 객체의 property를 공유 상태라고 한다.

다음은 sum함수가 접근하는 객체의 프로퍼티인 x가 공유상태인 것이다.

const x = {
  val: 2
};

const sum = () => x.val += 1;
const mul = () = x.val *= 2;

sum(); // 3
mul(); // 6
console.log(x.val); // 6


const y = {
  val : 2
};

const sum = () => y.val += 1;
const mul = () = y.val *= 2;

mul(); // 4
sum(); // 5
console.log(y.val); // 5

이 공유상태에 우리가 sum, mul을 순차적으로 한다고 했을 때 나오는 결과값은 항상 6일 것이다.

그러나 y의 경우 어떠한 이유로 sum호출이 지연되어 mul -> sum 순으로 호출되었을 때 원하는 결과값을 얻을 수 없다.

만약 2라는 결과를 얻고 싶은데 6이 나왔다면 x나 y의 val값을 초기화하는 과정이 필요할 것이다. 이처럼 공유상태를 갖게되면 함수호출결과값을 변경하는 경우도 있을 것이다.

순수함수를 사용하여 공유상태를 피한다면 호출 타이밍 또는 순서에 따라 결과를 변경시키지 않을 수 있다.

const x = {
  val: 2
};

const sum = x => Object.assign({}, x, { val: x.val + 1 });
const mul = x => Object.assign({}, x, { val: x.val * 2 });

console.log(sum(mul(x)).val); // 5


const y = {
  val: 2
};

mul(y);
sum(y);

console.log(sum(mul(y)).val); //5

sum과 mul함수에서 새로운 객체를 생성해 프로퍼티를 할당하므로 아까와 같은 공유상태가 발생하지 않아 원하는 만큼 함수호출을 할 수 있다.

또한 함수를 합성해 실행순서를 보장할 수 있다.


상태변화를 피해라

불변성을 지켜라 라는 말과 동일할 듯 한데,

불변성이란 어떤 값의 상태를 변경하지 않는 것을 뜻한다.

상태변경은 side effect를 가져오므로 함수형에서는 제한한다.

const apple = {
  watermelon: 'good',
}

function fruitHater (x) {
  x.watermelon = 'hate';
  return x;
}

console.log(apple.watermelon); // 'good'
fruitHater(apple);
console.log(apple.watermelon); // 'hate'

apple의 상태가 fruitHater에 의해 변경되어 불변성을 지키지 않는 코드이다.

const apple = {
  watermelon: 'good',
}

function fruitHater (x) {
  const other = {...x,
                watermelon: 'hate'};
  return other;
}

const other = fruitHater(apple);
console.log(apple.watermelon); // 'good'
fruitHater(apple);
console.log(apple.watermelon); // 'good'
console.log(other.watermelon); // 'hate'

apple의 프로퍼티를 변경하는 대신 새로운 객체를 할당해 부수효과를 없애어 불변성을 유지한 코드이다.

불변성을 왜 지키냐 ?

불변성을 지키지않은 위의 예시에서 fruitHater()가 어딘가에 숨어있다고 생각해보자.

그럼 우리는 x값은 분명 초기에 good이었는데 어느샌가 hate로 바뀌어 있다는 것만 알 수 있다. fruitHater()를 찾기전까지 !! 그게 수 천줄이라고 생각하면 아득하다.

따라서 불변성을 지킨다면 나 뿐만아닌 다른 사람이 보았을 때 x라는 데이터가 변하지 않는다는 생각을 기반으로 둔채 코드를 읽어내려갈 수 있다.

결론적으로 함수형프로그래밍은 위의 특징들을 지키며 가장 핵심인 부분은 순수함수를 이용해 side effect를 줄이고, 만약 외부변수에 접근하거나 수정되는 일이 발생한다면 그 부분은 순수함수내에서 분리하여 작성하도록 한다. 또한, 상태전달은 불변으로 관리하여 유지보수를 쉽도록 지향한다. 위의 특징들을 지키며 함수 자체를 변수에 넣고 인자로 전달할 수 있으며, 함수를 반환하는 js의 특성을 생각하면서 읽기 쉬운코드를 짤 수 있도록 생각을 열어두자!

참고

0개의 댓글