함수형 프로그래밍을 공부하기 위한 기초 지식들

dante Yoon·2022년 3월 5일
17

fp

목록 보기
2/2
post-thumbnail

책 함수형 자바스크립트 프로그래밍(유인동 저)를 읽으며 정리한 내용입니다.

들어가며

우리는 스마트폰과 축약어라는 도구에 너무나 익숙해지는 바람에 이전 세대에서 사용하던 일상적인 용어 사용에 혼동을 겪게 되는 경우를 종종 발견한다.

이 시대의 프로그래밍 또한 마찬가지이다.
우리는 프레임워크라는 바퀴에 너무나 익숙해지는 바람에 바닐라 자바스크립트가 주는 언어적 활용성을 스스로 사용하여 움직이는 법에 대해 망각해버리는 자신을 종종 발견하곤 한다.

함수형 프로그래밍은 자바스크립트 언어를 활용하고 응용하는 능력을 길러주는 좋은 패러다임이다.

일급함수

일급 - 값으로 다룰 수 있다.

  • 변수에 담을 수 있다.
  • 함수나 메서드의 인자로 넘길 수 있다.
  • 함수나 메서드에서 리턴할 수 있다.

일급 함수는 아래의 조건을 추가로 만족해야 한다.

  • 아무 때나 (런타임에서도) 선언이 가능하다.
  • 익명으로 선언할 수 있다.
  • 익명으로 선언한 함수도 함수나 메서드의 인자로 넘길 수 있다

(function(a,b) {return a + b;})(10,5);
function callAndAdd(a, b) {
  return a() + b();
}

callAndAdd(function() {return 10;}, function() {return 5;});
  • 함수는 인자로 받은 함수를 실행할 수 있고 함수를 리턴할 수 있다.
  • 메서드를 가진 객체와 달리 자기 자신이 곧 기능인 함수는
    • 쉽게 참조할 수 있고
    • 쉽게 전달할 수 있고
    • 쉽게 실행할 수 있다

함수로 기능을 동작시키는 것은 만들어 둔 클래스의 인스턴스를 생성하고 다루면서 기능을 동작시키는 것보다 간단하고 쉽다.

클로저

왜 이름을 이따구(?)로 지었는지 아직도 모르겠는 개념이다.
reference guard, scope pool 이런 식으로 지으면 좀 더 명확하지 않나.

스코프 - 변수를 어디에서 어떻게 찾을지를 정한 규칙

함수는 변수 참조 범위를 결정하는 중요한 기준이다.

클로저는 외부 환경을 기억하는 함수이다.

여기서 말하는 외부 환경은 변수다. 왜냐면 함수에서 기억해야 할 외부 환경이란 변수밖에 없기 때문이다.

이게 또 무슨 말이냐면, 생성된 함수는 항상 상위 스코프를 가지고 있다. 상위 스코프에서 생성된 변수를 참조할 일이 있기 때문에 '환경'이라는 단어가 생긴 것이다.

클로저는 자신이 생성될 때의 스코프에서 알 수 있었던 변수를 기억하는 함수다.

변수의 값은 항상 변할 수 있기 때문에 클로저에서 참조하는 변수값 또한 변경될 수 있다.

기술면접을 위한 기출 변형

모든 함수는 클로저인가?
함수 myFn이 클로저라면, myFn 내부에서 사용하고 있는 변수중에 myFn 외부에서 선언된 변수가 있어야 한다. 모든 함수는 상위 스코프를 가지고 있기 때문에 상위 스코프의 변수를 참조한다면 해당 함수는 클로저다.

정리하자면

클로저는 자신이 생성될 때의 스코프에서 알 수 있었던 변수 중 언젠가 자신이 실행될 때 사용할 변수들만 기억하여 유지시키는 함수다.

다음의 코드에서 에러 발생 유무를 예측해보라

function f9() {
  var a = 10;
  var f10 = function(c) {
    return a + b + c;
  }
  var b = 20;
  return f10;
}
var f11 = f9();
f11(30);
// 60

에러가 발생하지 않는다.
f10이 생성되기 딱 이전 시점에는 b가 20으로 초기화 되지 않았지만, 클로저는 자신이 생성되는 스코프의 모든 라인, 어드 곳에서 선언된 변수든지 참조하고 기억할 수 있다.

클로저는 자신이 생성되는 스코프의 실행 컨텍스트에서 만들어졌거나 알 수 있었던 변수 중 언젠가 자신이 실행될 때 사용할 변수들만 기억하는 함수이다. 클로저가 기억하는 변수의 값은 언제든지 남이나 지신에 의해 변경될 수 있다.

클로저의 실용 사례

클로저의 실용성은 은닉에 있지 않다.

  • 이전 상황을 나중에 일어날 상황과 이어 나갈 때
  • 함수로 함수를 만들거나 부분 적용을 할 때

Array.prototype.map 함수 대신 직접 map 함수를 생성해서 사용한다.

_.map = function(list, iteratee) {
  var new_list = [];
  for(var i = 0, len = list.length; i < len; i++) {
    new_list.push(iteratee(list[i], i, list));
  }
  return new_list;
}

codesandbox



const users = [
  { id: 1, name: "Dante", age: 20 },
  { id: 2, name: "John", age: 25 },
  { id: 3, name: "Peter", age: 30 }
];

document.getElementById("app").innerHTML = `
  <div class="user-list"></div>
`;

// 1. 클로저를 이용함
document.getElementsByClassName("user-list")[0].append(
  ..._.map(users, function (user) {
    var button = document.createElement("button");
    button.textContent = user.name;
    button.onclick = function () {
      console.log("user: ", user);
    };
    return button;
  })
);
document
  .getElementsByClassName("user-list")[0]
  .append(document.createElement("br"));


// 2. 클로저를 잘못 사용함
for (var i = 0; i < users.length; i++) {
  var button = document.createElement("button");
  console.log(users[i]);
  button.onclick = function () {
    console.log(i);
    console.log("user: ", users[i]);
  };
  button.textContent = users[i].name;
  document.getElementsByClassName("user-list")[0].append(button);
}

사진은 버튼을 왼쪽에서 오른쪽으로, 위에서 아래 순으로 차례대로 눌렀을때 출력되는 내용을 나타낸 것이다.

질문: 위, 아래 버튼 셋은 어떤 차이점으로 이러한 콘솔 결과를 내는 것일까?

  1. _.map을 사용하는 첫 번째 예제에서는 for문을 돌 때마다 iteratee를 실행시켜서 새로운 execution context를 만들기 때문에 의도한 대로 동작한다.
  1. 아래 코드 또한 클로저를 사용하긴 하지만 for 문을 돌면서 클로저가 참조하는 환경(변수)가 변경되기 때문에 의도한대로 동작하지 않는다.

1번을 읽고도 이해가 잘 안될 수 있다. execution context와 연관지어서 다시 한번 깊이 알아보자.

고차함수

  • 함수를 인자로 받아서 실행하는 함수
  • 함수를 반환하는 함수
  • 함수를 인자로 받아서 다른 함수로 반환하는 함수

함수를 리턴하는 함수

function constant(val) {
  return function() {
    return val;
  }
}

var value = 10;
var always10 = constant(value);
always10() // 10;
value = 20;
always10() // 10;

함수를 대신 실행시켜주는 함수

function callWith(val) {
  return function(val2, func){
    func(val,val2);
  }
}

calledWith10 = callWith(10);

function minus(val,val2) {
  return val - val2
}

calledWith10(5,minus); // 5

콜백 함수

콜백 패턴은 함수가 일을 마친 후 콘텍스트를 다시 돌려주는 경우를 말한다. 콜백 함수는 콜백 함수를 인자로 받는 함수가 종료될 때 한번 호출된다.

아래의 경우는 콜백 함수가 아니다.

// 이벤트 리스너, 실행 컨텍스트를 돌려주지 않는다. 
button.click(function(e) {
  console.log(e.target.value);
});

// iteratee, _map 함수가 iteratee를 내부적으로 여러 번 사용한다.
_.map(list, function() {
  // something
});

콜백함수를 인자로 받는 함수도 고차 함수이다.

유명 함수(named function)

const fv = function f1() {
  console.log(f1)
}

함수를 값으로 다루면서 익명이 아닌 f1 같은 함수를 named function이라고 한다.
named function을 사용하게 되면 함수가 자기 자신을 가르키기 편하다.

call, apply, this, arguments

  • ()
  • call, apply
function test(a,b,c) {
  console.log(this);
  console.log(a,b,c);
  console.log(arguments);
}

test(10);
 // Window
// 10, undefined, undefined,
// [10]
test(10,undefined);
// Window
// 10, undefined, undefined,
// [10, undefined]
test(10,undefined,undefined);
// Window
// 10, undefined, undefined,
// [10, undefined, undefined]

위의 코드를 실행해보면 b,c 는 항상 undefined가 표시되는 반면, arguments는 명시적으로 undefined가 넘겨지지 않으면 표시되지 않는 것을 알 수 있다.

인자와 arguments 링크

function link(a,b) {
 b = 10;
 console.log(b);
 console.log(arguments);
}

link(5);
// 10
// [5]

link (5,2);
// 10
// [5, 10]

arguments[1] 값이 넘어왔을 때 함수 내부에서 인자와 arguments[1]은 서로 링크되어있다. 따라서 b의 값이 바뀌면 arguments[1]의 값도 바뀐다.

반대의 경우도 마찬가지다.

function link(a, b) {
  arguments[1] = 5;
  console.log(b);
}

link(5, 2);
// 5 

this

function test(a,b,c) {
  console.log(this);
}

test(10);
 // Browser: Window, Node: global
test(10,undefined);
 // Browser: Window, Node: global
test(10,undefined,undefined);
 // Browser: Window, Node: global

어떻게 this를 Window 객체가 아닌 다른 값으로 변경할 수 있을까?

메서드로 만들기

// 앞서 정의한 test function을 사용
...
o1 = {
  a: 1,
  b: 2
}

o1.test = test;

o1.test();
// {a: 1, b: 2, test: ƒ test()}

자바스크립트에서는 객체의 속성으로 this를 참조하는 함수를 붙이면, .메소드 왼쪽의 객체가 this가 된다.

// 앞에 정의한 o1 객체를 사용한다.
const o1_test = o1.test;
o1_test()
// Window

this를 실행 시킨 컨텍스트가 글로벌 콘텍스트이기 때문에 Window 객체가 나온다. 어떻게 선언했는가가 중요한 것이 아니라, 어떻게 실행시켰는지에 따라 this의 값이 변경된다.

(o1.test)(1,2);
// {a: 1, b: 2, test: ƒ test()}
(o1['test'])(1,2);
// {a: 1, b: 2, test: ƒ test()}

함수 실행의 괄호

일반 괄호

(5)
(function f() {
  return 5;
})

f()
// Reference error

일반 괄호에서 코드가 실행되면, 해당 공간에서 값이 만들어지고 끝난다. 다른 곳에서 해당 값을 참조할 수 없다.

함수를 실행시키는 괄호

var add5 = function(a) {
  return a + 5;
}

var call = function(fn) {
  return fn();
}

add5(10); // 함수를 실행시키는 괄호
call(console.log('hi')) // 함수를 실행시키는 괄호

함수를 실행시키는 괄호를 통해 새로운 실행 컨텍스트가 생성된다. 이 부분은 매우 중요하다. 해당 지점에 값을 만든 후 끝나지 않고 이 값이 실행된 함수에게 넘어간다. 이 공간의 실행이 끝나기 전까지는 이전의 공간의 상황들도 끝나지 않는다.

화살표 함수

화살표 함수는 ES6부터 지원되는 문법이다.

익명함수와 비교

1. 문법 비교

한 줄 짜리 함수

var add =function(a,b){
  return a + b; 
}

var add = (a,b) => a + b;

두 줄 짜리 함수

var add2 = function(a,b) {
  var result = a + b;
  return result;
}

var add2 = (a,b) => {
  var result = a +b;
  return result
}

화살표 함수를 사용하며 화살표가 중첩되는 문법은 처음 보기에는 난해할 수 있으나 눈에 익숙해지면 가독성이 좋고 사용하기도 편하다.

함수 리턴 타입이 void가 아니라면, 화살표 함수를 사용하는 것이 훨씬 간단하다.

var identity = function(v) {
  return v;
}

var identity = v => v;

var constant = function(v) {
  return function() {
    return v;
  }
}

var constant = v => () => v;

2. 기능 비교

2019년 겨울 첫 정규직 기술 면접 때 화살표 함수와 익명 함수의 차이점을 잘 대답하지 못한 적이 있다.

기술면접을 앞두고 있다면 이 부분을 잘 참고하라

화살표 함수의 또 다른 특징은 함수 내부의 this와 arguments는 항상 부모 함수의 this, arguments로 고정된다는 점이다.

여기서 부모 함수는 화살표 함수를 둘러싸고 있는 여러 계층의 함수 중 화살표 함수가 처음으로 만나는( 가장 가까운 거리에 있는 ) 일반 함수를 말한다.

(function(){
  console.log(this,arguments);
  // {hi: 1} [1,2,3]
  (() => {
    console.log(this.arguments)
    // {hi: 1} [1,2,3]
    (() => {
      console.log(this, arguments);
      // {hi: 1} [1,2,3]
    }) ()
  }) ()
}).call({hi:1},1,2,3);

실용 사례

화살표 함수 재귀

var start = f => f(f);
var logger = (a,b) => start(f => console.log(a) || a++ == b || f(f));
logger(6,10);
// 6
// 7
// 8
// 9
// 10

이해가 안된다면 아래 코드를 보자

var start = f => {
  console.log(f)
  return f(f);
};
var logger = (a,b) => start(f =>  console.log(a) || a++ == b || f(f));
logger(6,10);
// f =>  console.log(a) || a++ == b || f(f)
// 6
// 7
// 8
// 9
// 10
// true 

이해가 안된다면 아래 코드를 보자

var start = f => {
  console.log(f)
  return f(f);
}
var logger = (a,b) => start(f => console.log(a) || a++ == b || f(f));
logger(6,10);
// f => console.log(a) || a++ == b || f(f)
// f(f);
// console.log(6)
// 6++ == 10
// f(f);
// console.log(7)
// 7++ == 10
// f(f);
// console.log(8)
// f(f);
// console.log(9);
// f(f);
// console.log(10);

아무 곳에서나 함수 열기, 함수 실행을 원하는 시점으로 미뤄서 실행하기를 자유롭게 할 수 있는가?

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글