함수형 프로그래밍

Posted by yunki kim on March 26, 2022

  JS에서 객체지향 패러다임을 사용하기는 쉽지 않다. JS는 프로토타입 기반 객체지향을 사용하고 this 역시 자바 같은 언어에서의 this와는 다르기 때문이다. 따라서 객체지향 대신 함수형을 사용한다면 프로젝트의 유지보수가 조금은 더 쉬워지고, 디버깅도 쉬워진다. 

  함수형 프로그래밍은 모든 것을 함수로 표현한다. 따라서 함수형 프로그래밍에서는 함수의 역할(입력을 받아 아웃풋을 내보내는 것)을 고려하면 된다. 명령형 프로그래밍과는 다르게 동작을 위해 동작의 구현 과정까지 세세히 생각하지 않아도 된다. 즉, 함수형 프로그래밍은 어떤 동작을 수행하기 위해 동작 과정을 세세히 명령하는 것이 아닌, "어떤 동작을 하겠다!" 라고 선언해야 한다.

  다음 예제를 보자.

1
2
3
const name = "yunki";
const introduction = "Hi, I'm ";
console.log(introduction + name);
cs

  이 코드는 함수형 프로그래밍이 아니다. 그 이유는 자기 소개를 출력하기 위해 이름과 자기 소개 문구를 세세히 명령 했기 때문이다.

1
2
3
4
function introduce(name) {
  return "Hi, I'm " + name;
}
console.log(introduce("yunki"));
cs

  그에 반해 위의 코드는 함수형 프로그래밍이다. 그 이유는 자기 소개를 출력하기 위해 세세한 과정을 명시하는 것이 아닌, introduce 함수를 이용해 "자기소개"를 선언 했기 때문이다.

함수형 프로그래밍의 특징

순수함수(pure function)

  함수형 프로그래밍에서 다루는 함수는 순수 함수다. 순수함수란 오직 주어진 입력만을 사용해 값을 출력하는 함수를 의미한다. 따라서 오직 지역 변수의 값만 변경할 수 있으며 이를 "side-effect가 없다"라고 한다.

  다음 예시를 보자. 이 예시는 전역 변수에 접근하기 때문에, 즉 , 주어진 입력 외의 것들에 접근하기 때문에 순수함수가 아니다.

1
2
3
4
5
const name = "yunki"
function introduce() {
  return "Hi, I'm " + name;
}
 
cs

고차함수(higher-order function)

  함수를 일급 시민으로 사용해 함수를 입력으로 사용하거나 결과로 반환하는 것을 의미한다. 고차 함수를 사용하면 다음과 같은 동작이 가능해진다.

1
2
3
4
5
6
7
function makeAdjectifier(adjective) {
  return function (string) {
    return adjective + " " + string;
  };
}
const coolifier = makeAdjectifier("cool");
coolifier("conference"); // cool conference
cs

  다른 프로그래밍 패러다임에서 사용하는 기법들을 사용하지 않으려면 고차 함수를 항상 염두해 두어야 한다.

일급 시민 (first-class citizen)

  함수형 프로그래밍에서는 함수를 1급 시민으로 대한다. 따라서 함수를 다음과 같이 사용할 수 있다.

    1. 함수를 함수의 인자로 받을 수 있다.

    2. 함수를 반환값으로 활용할 수 있다.

    3. 자료구조에 저장할 수 있다.

 

함수형 프로그래밍에서 피해야 할 사항들

반복하지 마라

  다른 프로그래밍 패러다임에서는 리스트나 배열의 원소들을 이용해 특정 동작을 하기 위해 while같은 반복문을 사용하곤 한다. 하지만 고차 함수를 사용하면 불필요한 반복을 없앨 수 있다.

  배열을 순환하면서 짝수를 골라내는 로직을 반복문을 사용하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
const numbers = [123456];
let evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  const squareNumber = numbers[i] * numbers[i];
  if (squareNumber % 2 === 0) {
    evenNumbers.push(squareNumber);
  }
}
 
for (let i = 0; i < evenNumbers.length; i++) {
  console.log(evenNumbers[i]); // 4, 15, 36
}
cs

  이를 고차함수를 통해 수정하면 다음과 같이 된다.

1
2
3
4
5
numbers
  .map((number) => number * number)
  .filter((number) => number % 2 === 0)
  .forEach((number) => console.log(number));
 
cs

  코드가 훨씬 간결해졌다! 여기서 map, filter는 for, while과는 다르게 순환하는 원소들을 소비(consume)한다. 즉, for와 while은 반복문 내부에서 원한다면 이미 순환한 원소로 되돌아 갈 수 있다. 그에 반해 map, filter 같은 고차 함수는 원소들을 소비하기 때문에 한번 소비한 원소로 다시 되돌아갈 수 없다.

mutability를 피하라

  함수형 프로그래밍에서는 같은 이수값으로 함수를 호출하면 항상 같은 값을 반환해야 한다. 여기서 만약 데이터가 가변이라면 원치 않는 변경으로인한 버그가 발생할 수 있다. 

  다음 예시는 불변과 가변의 예시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 가변
let numbers = [123456];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    numbers[i] = numbers[i] * numbers[i];
  }
}
 
// 불변
numbers = numbers.map(number => {
  if (number % 2 === 0) {
    return number * number;
  } else {
    return number;
  }
});
cs

영속 자료 구조(Persistent data structures)

  불변은 계속 복사를 해야 한다는 단점을 가지고 있다. 예를 들어 위의 numbers 코드의 경우, 바뀔 필요가 없는 홀수 숫자도 복사를 한다. 만약 데이터의 크기가 커진다면 성능 문제가 발생할 수 있다. 영속 자료 구조를 사용하면 이 문제를 해결할 수 있다. 

  영속 자료 구조는 데이터가 수정되었을 때 이전 버전을 보존하고 있는 자료 구조다. 따라서 기존 버전의 데이터가 수정되지 않으므로 불변이다.

  만약 모든 버전의 데이터에 접근할 수 있지만 최신 버전의 데이터만 수정될 수 있다면 이 자료 구조는 부분적으로 영속적이다. 만약 모든 버전의 데이터에 접근할 수 있고 수정할 수 있다면 이 자료 구조는 완전히 영속적이다.

  불변을 위해서는 공통되는 부분까지 새롭게 복사해야 한다. 따라서 성능문제가 존재한다. 이 문제는 이전 버전과 새로운 버전 간의 유사성을 이용해 서로 구조를 공유하면 해결된다.

  예를 들어 이진 탐색 트리에 새로운 노드를 불변으로 추가한다 해보자. 이 때, 루트 부터 새로 추가된 노드까지의 경로만 새로 생성하고, 나머지는 기존 버전을 생성해 영속 자료 구조로 구현하면 다음과 같이 된다.

초기 버전

노드 E가 추가된 버전