[JavaScript] 3-3. JavaScript functional Programming

JavaScript functional Programming


고차 함수 (Higher-order Function)

함수를 인수로 받는 함수, 또는 함수를 반환하는 함수를 일러 고차 함수라고 합니다.

// 함수를 인수로 받는 함수
function moong1(moong2) {
  moong2();
}

// 함수를 변환하는 함수
function moong1() {
  return moong2() {};
}

예를 들어, 함수를 인수로 받는 Array 의 많은 메소드들 (filter, sort, map, forEach, reduce, every, some..)은 고차 함수 입니다.
다른 함수의 인수로 함수를 넘겨지는 함수를 콜백이라고 합니다.



클로져 (Closure)

보통의 경우, 안쪽 스코프에서 정의된 변수는 바깥 스코프에서 사용될 수 없습니다.

function func1(x) {
  return x;
}

func1(1);
// 더 이상 변수 'x'에 접근할 수 있는 방법이 없습니다
for (let i = 0; i < 10; i++) {
  console.log(i);
}
// 더 이상 변수 'i'에 접근할 수 있는 방법이 없습니다

하지만, 안쪽 스코프에서 만들어진 함수에서 바깥 스코프의 변수를 사용하고 싶다면,
이 함수를 통해서 변수를 계속 사용할 수 있습니다.
심지어 바깥 스코프에 해당하는 코드의 실행이 끝난 뒤라도 말입니다.

function func1(x) {
  // 여기서 반환되는 함수는 바깥 스코프에 있는 변수 'x'를 사용하고 있습니다.
  return function() {
    return x;
  };
}

const func2 = func1(1);

// func1의 실행은 끝났지만, func2를 통해서 변수 x를 사용할 수 있습니다.
console.log(func2()); //  1
cosnt arr = [];

for (let i = 0; i < 10; i++) {
    // 여기서 만들어진 함수는 바깥 스코프에 있는 변수 'i'를 사용하고 있습니다.
    arr.push(function() {
        return i;
    })
}

// for 루프의 실행은 끝났지만, 루프 안에서 만들어진 함수가 배열에 저장되어 있습니다.
// 배열 안에 들어있는 함수를 통해, 해당 함수가 만들어진 시점의 변수 'i'를 사용할 수 있습니다.
console.log(arr[2]());  //  2
console.log(arr[5]());  //  5

이처럼, 바깥 스코프에 있는 변수를 가져다 사용하는 함수와 변수가 저장되는 저장소를 클로저라고 합니다.
클로저의 성질을 통해 고차 함수를 여러 방법으로 활용할 수 있습니다.

// 고차 함수의 인수로 함수를 넘길 때, 해당 함수에서 바깥 스코프에 있는 변수를 사용할 수 있습니다.
const people = [{ name: "moong2", age: 29 }, { name: "min_zzz", age: 24 }];

function whoIsOlder(people, threshold) {
  return people.filter(person => person.age > threshold);
}

whoIsOlder(people, people[1].age);
// 특정한 방식으로 동작하는 함수를 만들어내는 고차 함수를 작성할 수 있습니다.
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

[1, 2, 3].map(makeAdder(2)); //  [3,4,5]

때때로 클로저의 성질은 데이터를 숨기고 정해진 방법을 통해서만 데이터에 접근할 수 있도록 제한을 두는 데 활용되기도 합니다.

function makeCounter(x = 1) {
  return function() {
    return x++;
  };
}
// 'x'를 직접 변경할 방법이 없습니다.

const counter = makeCounter();
console.log(counter()); //  1
console.log(counter()); //  2
function gettingOlder(yourage) {
  let age = yourage;

  return {
    getOlder() {
      age++;
    },
    getAge() {
      return age;
    }
  };
}
// 'age'를 직접 변경할 수 있는 방법이 없습니다.

const me = gettingOlder(29);
me.getOlder(); // 29++
me.getAge(); // 30...ㅠㅠ

근래의 JS 디버거는 클로저에 어떤 값이 들어있는 지를 보여주는 기능을 포함하고 있습니다.



화살표 함수와 고차 함수

화살표 함수 문법을 이용하면, 적은 양의 코드만 사용해서 고차 함수를 만들 수 있습니다.

// const makeAdder = x => (y => x + y);
// 화살표함수가 여러개가 나온다면 맨 우측에 있는 식부터 계산후 반환이라고 생각하면 됩니다!
const makeAdder = x => y => x + y;

// 밑의 코드와 위의 한줄과 같습니다.
// function makeAdder (x) {
//   return function (y) {
//     return x + y;
//   }
// }

const add2 = makeAdder(2);
add2(3); //  5
// makeAdder(2)(3)과 같습니다.

// 번외
const makeAdder = x => y => z => x + y + z;
makeAdder(2)(3)(4);
// 길게 길게 쓸수도 있습니다.
const makeCounter = (x = 1) => () => x++;

// 밑의 코드와 위의 한줄과 같습니다.
// function makeCounter(x = 1) {
//   return function() {
//     return x++;
//   };
// }

const counter = makeCounter();
console.log(counter()); //  1
console.log(counter()); //  2



재귀 함수 (Recursive Function)

함수 내부에서 자기 자신을 호출하는 함수를 재귀 함수라고 부릅니다.

function func() {
  func();
}

// 무한 루프로 빠져든다아..

이와 같이 재귀 함수 자체는 무한루프적인 특징이 있습니다. (꼭 무한루프를 탈출할수있도록 장치를 만들어야 합니다.)



재귀 함수 - 루프와 재귀 함수

대부분의 루프는 재귀함수를 통해 다시 구현될 수 있습니다.

// 루프로 구현된 팩토리얼
function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

// 재귀함수로 구현된 팩토리얼
function factorialRec(n) {
  return n <= 1 ? 1 : n * factorialRec(n - 1);
}
// 루프로 구현된 피보나치 수열
function fiboLoop(n) {
  let x = 0;
  let y = 1;
  for (let i = 0; i < n; i++) {
    [x, y] = [y, x + y];
  }
  return x;
}

// 재귀 함수로 구현된 피보나치 수
function fiboRec(n) {
  return n < 1 ? 0 : n === 1 ? 1 : fiboRec(n - 1) + fiboRec(n - 2);
}

이처럼 재귀 함수를 사용하면 루프를 사용했을 때보다 코드의 의미가 명확해지고 코드의 길이를 줄일 수 있다는 장점이 있습니다.



재귀 함수 - 분할 정복 (Divide and Conquer)

분할 정복은 문제를 작은 부분 문제로 나누어서 푼 뒤, 그 결과를 합치는 식으로 알고리즘을 작성하는 기법이며,
재귀 함수가 활용되는 대표적인 사례입니다. 바로 위의 fiboRec 역시 분할 정복의 일종이라고 할 수 있습니다.

분할 정복 기법을 활용하는 알고리즘 중 대표적인 예로 병합 정렬(merge sort)이 있습니다.

function mergeSort(arr) {
  // 입력된 배열의 길이가 1 이하이면 더 이상 재귀 호출을 하지 않습니다.
  if (arr.length <= 1) return arr;

  // 배열을 절반으로 잘라 두 개의 작은 배열로 분할하고,
  // 두 작은 배열에 대해 재귀 호출을 수행합니다.
  const slicer = Math.floor(arr.length / 2);
  const arr1 = mergeSort(arr.slice(0, slicer));
  const arr2 = mergeSort(arr.slice(slicer));

  // 'arr1', 'arr2'는 **이미 정렬되어 있는 상태**이므로,
  // 이 성질을 이용해 두 배열을 **정렬되어있는 큰 배열**로 합칠 수 있습니다.
  const newArr = [];
  for (let i = 0, j = 0; i < arr1.length || j < arr2.length; ) {
    if (arr1[i] == undefined || arr1[i] > arr2[j]) {
      newArr.push(arr2[j]);
      j++;
    } else {
      newArr.push(arr1[i]);
      i++;
    }
  }

  // 큰 배열을 반환합니다.
  return newArr;
}



재귀 함수 - 주의 사항

재귀 함수가 실행되는 동안에는 종료되지 않은 함수가 아주 많이 생기게 되므로,
코드의 실행 속도가 느려지거나 컴퓨터 메모리에 부담을 줄 수 있습니다.
이 때문에 대부분의 JS 구동 환경에서는 특정 깊이 이상의 재귀 호출이 일어날 수 없도록 제한을 두고 있습니다.
chrome 의 경우는 대략 1 만번 정도의 재귀 호출이 일어나면 에러를 발생시킵니다.

factorialRec(20000); // RangeError : Maximum call stack size exceeded

재귀 호출에 대한 제한 때문에 원하는 작업을 할 수 없게 된다면 재귀 함수 대신 루프 혹은 스택(stack)을 사용하면 됩니다.
또한, 주의하지 않으면 쓸때없이 재귀 호출이 많이 일어나게 될 수 있습니다.(같은 인수에 대한 호출이 쓸때없이 증가)
사실 함수를 호출하여 답을 한번 구했다면, 그 답을 알고 있으므로 다시 계산할 필요가 없습니다.
그래서 한 번 구해놓은 답은 별도의 저장소에 기억하고,
후에 같은 인수로 함수가 호출되면 이전에 계산해놓았던 답을 바로 반환하는 식의 재귀함수를 작성할 수 있습니다.
이런 방식을 통해 함수의 호출 횟수를 극단적으로 줄일 수 있습니다.
이런 기법을 메모이제이션(memoization)이라고 합니다.

const fiboRecMemoized = (() => {
  // 계산 결과를 저장하는 저장소를 만듭니다.
  const memo = new Map();

  const fiboRec = n => {
    // 만약에 이전에 같은 인수로 계산한 적이 있다면
    // 해당 결과를 바로 반환합니다.
    let result = memo.get(n);
    if (result != undefined) return result;

    result = n < 1 ? 0 : n === 1 ? 1 : fiboRec(n - 1) + fiboRec(n - 2);

    // 계산 결과를 저장소에 저장한 후 반환합니다.
    memo.set(n, result);
    return result;
  };

  return fiboRec;
})();

다음은 memoization 을 했을때와 안했을 때의 차이입니다.

function fiboRec(n) {
  return n < 1 ? 0 : n === 1 ? 1 : fiboRec(n - 1) + fiboRec(n - 2);
}

const fiboRecMemoized = (() => {
  const memo = new Map();
  const fiboRec = n => {
    let result = memo.get(n);
    if (result != undefined) return result;
    result = n < 1 ? 0 : n === 1 ? 1 : fiboRec(n - 1) + fiboRec(n - 2);
    memo.set(n, result);
    return result;
  };
  return fiboRec;
})();

console.time("fiboRec");
fiboRec(40); //  1417ms
console.timeEnd("fiboRec");

console.time("fiboRecMemoized");
fiboRecMemoized(40); //  0ms
console.timeEnd("fiboRecMemoized");



2. Today I Found Out

이제 조금씩 나이도가 있는 내용에 접어들면서 미리 예습하지 않고
생각하지 않으면 이해하기 조금은 어려운 부분들이 등장하고 있습니다.
미리 공부를 하면서 이해가 안되는 부분은 체크해 놓고
내일 있을 수업을 들으면서 이해를 했던 부분이 확실하게 맞고
이해가 잘 되었는지, 또 이해가 안된 부분은 적극적으로
질문을 통하여 확실히 이해를 할 수 있도록 해야겠습니다!



3. refer

https://helloworldjavascript.net/pages/255-fp.html

https://homoefficio.github.io/2015/07/27/%EC%9E%AC%EA%B7%80-%EB%B0%98%EB%B3%B5-Tail-Recursion/

https://www.zerocho.com/category/JavaScript/post/576cafb45eb04d4c1aa35078

https://github.com/indongyoo/functional-javascript/wiki/1.2-%ED%95%A8%EC%88%98%ED%98%95-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EC%8B%A4%EC%9A%A9%EC%84%B1