본문으로 바로가기

Closure, Currying, Compose

category software engineering/javascript 2023. 1. 7. 21:30
728x90

Closure 클로저

function welcome(id) {
  return function () {
    console.log('hello ' + id);
  }
}

const greet = welcome('bruce');
greet();

클로저를 생성하는 방법은 함수가 다른 함수 내부에서 선언되고, 나중에 실행되는 것입니다. 함수가 실행될 때 새 실행 컨텍스트가 생성되고 실행 스택에 아래 이미지와 같이 쌓입니다.

welcome('bruce'); 가 실행되면 해당 컨텍스트가 생성되어 변수와 lexical environment이 실행 스택에 쌓이고 실행이 완료되면 welcome() 실행 컨텍스트가 종료됩니다.

함수가 종료되며 스택에서 해당 실행 컨텍스트가 완전히 떨어저 나가지만 정의된 함수가 있기에 클로저가 활성화되고 메모리 어딘가에 lexical environment는 함수의 정의와 함께 살아 있습니다.

클로저가 활성화 되어 메모리 어딘가에 살아있음

const greet = welcome('bruce');
greet()

그리고 greet()이 실행되면 greet()에 대한 실행 컨텍스트가 아래와 같이 스택에 생성됩니다. 그리고 greet()의 실행 컨텍스트에서 스코프 체인 프로세스를 통해 id를 찾아보고 없기 때문에 외부 환경(outer environment)에서 찾아봅니다. 이 때 클로저로 인해 있던 id를 참조할 수 있게 됩니다.

스코프 체인 프로세스로 id를 참조한다.

 

또 다른 예로,

function getFunctions() {
  var arr = [];
  
  for (var i = 0; i < 3; i++) {
      
    arr.push(
      function() {
        console.log(i);   
      }
    )
      
  }
  return arr;
}

const fs = getFunctions();

fs[0]();
fs[1]();
fs[2]();

의 결과가 0, 1, 2라고 생각했다면 위의 코드를 같이 분석해보자.

getFunctions()의 변수와 lexical 환경을 포함한 실행 컨텍스트가 스택에 추가됩니다. 

변수가 선언된 i=0에서 시작하여 for문을 한 줄씩 계속 읽고 arr에 함수를 저장합니다. 이 때, 다른 내부에 함수 선언이 있기 때문에 클로저가 활성화 됩니다. 클로저 기능이 실행되고 함수 실행이 완료되면 아래와 같이 getFunctions()는 스택에서 벗어났지만 클로저가 활성화 되어있기 때문에 변수와 함께 lexical environment 환경은 메모리에 남아 있게 됩니다.

 

그리고 다음 코드를 실행합니다.

const fs = getFunctions();

fs[0]();
fs[1]();
fs[2]();

이 때도 마찬가지로, 자신의 실행 컨텍스트인 fs[0]()의 실행 컨텍스트에서 변수를 찾지 못해 스코프 체인 프로세스로 인해 외부 환경에서 활성화 된 클로저 덕분에 찾을 수 있습니다. 하지만 메모리에 남아 있지만 실행 컨텍스트가 스택에서 지워지기 전 getFunctions()의 모든 반복을 수행한 뒤이므로 i=3으로 남아있습니다. fs[1](), fs[2]()도 똑같은 일이 발생합니다.

 

함수를 내부에 선언하고 저장하고 있을 뿐 실행하지 않았기 때문에 함수 i가 할당 되었지만 평가되지 않았습니다. 이 문제를 해결하기 위해서는 기본 값이 고유하고 다른 변수에 할당될 때 메모리에 별도로 기억해야 하기 때문에 각 반복에서 다른 컨텍스트를 만들고 변수 i를 선언해야 합니다.

function getFunctions() {
  var arr = [];
  for (var i = 0; i < 3; i++) {
    arr.push(
      (function(j) {
        return function() {
          console.log(j);   
        }
      })(i)
    )
  }
  return arr;
}

const fs = getFunctions();

fs[0]();
fs[1]();
fs[2]();

이번에는 이전과 달리 for문 내 push에서 함수가 실행되기 때문에 array안에서 아래와 같이 IIFE(즉시 호출 함수) lexical environment가 생성되어 위의 과정처럼 스코프 체인에 의해 outer environment를 참조해 0, 1, 2의 값을 얻게 됩니다.

 

Currying 커링 / Compose 컴포즈

위의 클로저를 이용해서 커링을 사용하면 매개변수와 매개변수를 반복한 이해하기 힘든 함수의 FP 선언형 방식으로서 가독성을 높일 수 있습니다.

const getResult = function(a, b) {
  return function(c) {
    return (a + b) * c;
  }
}

const sum = getResult(2, 3);

sum(2); // 10
sum(5); // 25
sum(8); // 40

// Not curried way
getResult(2, 3, 2);
getResult(2, 3, 5);
getResult(2, 3, 8);

그리고 이걸 화살표 함수로 리팩토링하면 const getResult = (a,b) => c => (a+b) * c; 로 나타낼 수 있다. 또 다른 예로, FP 선언형으로 이용하는데 나쁜 예와 좋은 예가 있다.

// bad
function map (arr, fn) {
  return arr.map(fn);
}

// good
const map = arr => fn => arr.map(fn);

// bad
items.filter(function(i){
  return i.name === 'Bruce';
}).map(function(i){
  i.lastname = 'Wayne';
});

// bad
const items = [{name: 'Bruce' }, {name: 'Fabs'}, {name: 'Bruce'}, {name: 'Gaby'}];
const filter = function(key, value){
  return function(i){
    return i[key] === value;
  }
}
const applier = function(key, value){
  return function(i){
    i[key] = value;
  }
}
const where = filter('name', 'Bruce');
const apply = applier('lastname', 'Wayne');
items.filter(where).map(apply);

// good
const items = [{name: 'Bruce' }, {name: 'Fabs'}, {name: 'Bruce'}, {name: 'Gaby'}];
const filter = w => a => a.filter(w);
const where = (k, v) => i => i[k] === v;
const map = fnMap => f => f.map(fnMap);
const applier = (k, v) => x => x[k] = v;

map(applier('lastname', 'Wayne'))(filter(where('name', 'Bruce'))(items))

이번에는 함수의 정의는 선언형으로 되었지만 호출하는 부분에서 이해하기가 어려워졌는데, 이 때 compose를 사용하면 쉽게 정리할 수 있다. 아래 코드는 js-doc 과 함께 위의 코드를 깔끔하게 정리한 코드이다.

// Data example
const items = [{name: 'Bruce' }, {name: 'Fabs'}, {name: 'Bruce'}, {name: 'Gaby'}];

/**
 * @description Filter by one where condition
 * @param w Where condition function
 * @returns {Function} filter function
 */
const filter = w => a => a.filter(w);

/**
 * @description Create a where condition function
 * @param k Criteria key name
 * @param v Criteria value
 * @returns {Function} Where condition function
 */
const where = (k, v) => i => i[k] === v;

/**
 * @description Create a map function
 * @param fnMap Function to set on map callback
 * @returns {Function} Map function
 */
const map = fnMap => f => f.map(fnMap);

/**
 * @description Create a modification function
 * @param k Key to set any value
 * @param v Value to set
 * @returns {Function} Modification function
 */
const applier = (k, v) => x => x[k] = v;

/**
 * @description Compose function to compose functions
 * @param fns Functions to reduce on composition
 * @returns {Function} Compose function
 */
const compose = (...fns) => x => fns.reduceRight((f, m) => m(f), x);

// Age and lastname compose function to apply changes
compose(
  map(applier('age', 33)),
  filter(where('name', 'Bruce'))
)(items);
compose(
  map(applier('lastname', 'Roque')),
  filter(where('name', 'Fabs'))
)(items);

// Print changes
console.log(items);

 

 

https://medium.com/sngular-devs/comprende-js-closures-4338eb2e46d6

 

Comprende JS: Closures

Parte VIII de la serie Comprende JS

medium.com

https://medium.com/sngular-devs/from-closures-to-curry-and-compose-197d2abcadd8

 

From Closures to Curry and Compose

First steps to understand one of FP features

medium.com