[TIL] 함수형 프로그래밍

Narastro·2021년 7월 28일
0

TIL

목록 보기
7/16
post-thumbnail

🏓 함수형 코드

함수형 프로그래밍 ?

프로그래밍 패러다임(Programming Paradigm)은 크게 명령형, 절차지향, 객체지향, 선언형, 함수형이 있다.

명령형 : 어떻게 할 것인지를 설명하는 방식
절차지향 : 수행될 순서에 따라
객체지향 : 객체들의 집합으로 프로그램의 상호작용을 표현
선언형 : 무엇을 할 것인지 설명하는 방식
함수형 : 순수 함수를 조합하여 소프트웨어를 만드는 방식

이 중에서 함수형 프로그래밍이란 거의 모든 것을 순수 함수로 나누어 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를을 통해 가독성을 높이고 유지보수를 용이하게 해준다.

함수형 프로그래밍의 특징

  • 부수 효과가 없는 순수 함수를 1급 객체로 간주하여 파라미터로 넘기거나 반환값으로 사용하며, 참조 투명성을 지킬 수 있다.

부수 효과(Side Effect)란

변수의 값이 변경되거나 자료 구조를 수정하거나 객체의 필드값 설정, 예외나 오류, IO 발생 등을 얘기한다.

순수 함수(Pure Function)란

이러한 부수 효과들을 제거한 함수들을 말한다. (예. 입출력에서 부수 효과가 없거나 함수의 실행이 외부에 영향을 끼치지 않는 함수)
이러한 순수 함수를 이용하면 함수 자체가 독립적이므로 스레드에 안정성을 보장할 수 있고 병렬 처리를 동기화 없이도 진행할 수 있다.

// 순수X (부수효과 발생)
function addTaco(array) {
   array.push("taco");
}

// 순수X (인자 대신 공유 변수를 이용)
function addTaco() {
   return [...globalArray, "taco"];
}

// 순수O
function addTaco(array) {
   return [...array, "taco"];
}

1급 객체란

변수나 데이터 구조 안에 담거나 파라미터로 전달되거나 반환값으로 사용될 수 있으며 고유한 식별이 가능한 객체를 말한다.

참조 투명성(Referential Transparency)

동일한 인자에 대해 항상 동일한 결과를 반환하는 것을 말한다. 이를 통해 기존의 값은 변경되지 않고 유지되며, 말 그대로 함수 실행 후 어떠한 상태 변화 없이 동일 결과를 반환하여 항상 결과를 투명하게 나타내므로 결과를 예측할 수 있다는 것으로 축약할 수 있겠다.

불변성

Immutable type

불변성(Immutability)란 말그대로 변하지 않는 것을 의미한다. 불변 데이터는 한번 생성되고나면 그 뒤에는 변할수 없다. 자바스크립트에서는 Boolean, String, Number, Null, undefined, Symbol과 같은 원시 타입이 있으며 이들은 메모리영역 안에서 변경이 불가능하며 변수에 할당할 때 완전히 새로운 값이 만들어져 할당된다.

자바스크립트에서 immutable type을 제외하고 모든 값은 object 타입이므로 배열도 엄밀히 말하면 object타입이다.

Const에 대해 짚어볼 것!

let 변수는 어떠한 값을 가리키다가 다시 다른 값의 주소를 가리키는 재 할당이 가능하다. 하지만 const는 재선언 및 재할당이 불가능하다.

구분해야될 개념으로 const로 선언한 변수는 값이 불변하는 것이 아니라 '참조가 불변'하는 것이다. 참조하고 있는 값은 불변하지 않는다. 쉬운 예로 배열을 const로 선언한 경우 문자열로 바꾸는 것은 불가능하지만 배열 내부의 값을 바꿀 수는 있다.

객체 내부의 깊은 곳은 재할당됨을 주의하자!

const car = {
  owner : "junimo",
  type : "truck"
};
car = {
  owner : "ken",
  type : "truck"
};

이는 에러를 발생시키나,

const car = {
  owner : "junimo",
  type : "truck"
};
car.owner = "ken";
console.log(car); // {owner: "ken", type: "truck"}

여기서는 car의 owner 프로퍼티가 변경되었다. car 자체는 재할당되지 않지만 car.owner는 재할당이 가능하다. 즉 객체 내부의 깊은 곳은 재할당이 제어되지 않음에 유의하자.

클로저(Closure)

이하 자바스크립트 딥다이브 내용을 공부하며 정리한 내용이다.

렉시컬 스코프

자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬(정적) 스코프라 한다.
함수 객체는 내부 슬롯에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다.

클로저

외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

다음 예를 보자.

const x = 1;

function outer() {
	const x = 10;
    const inner = function () { console.log(x); }
    return inner;
}

const innerFunc = outer();
innerFunc();

여기서 outer 함수의 렉시컬 환경은 inner 함수의 내부 슬롯에 의해 참조되고 있고 inner함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않는다. 따라서 이는 유지된다.

그렇다면 자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모두 클로저일까? 아니다. 상위 스코프의 식별자를 참조하지 않는 경우 일반적으로 클로저라고 하지 않는다.

function foo(){
	const x = 1;
    const y = 2;
    
    function bar() {
    	const z = 3;
        console.log(z);
    }
    return bar;
}

이러한 경우 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다. 메모리 낭비이기 때문이다.

또다른 예시로 중첩함수가 있다.

function foo(){
	const x = 1;
    const y = 2;
    
    function bar() {
    	const z = 3;
        console.log(x);
    }
    bar();
}

이 경우 bar의 생명 주기가 foo보다 짧다 따라서 이런 경우 bar는 클로저였지만 외부 함수보다 일찍 소멸되기 때문에 클로저의 본질에 부합하지 않는다. 따라서 이 경우도 일반적으로 클로저라고 하지 않는다.

이 경우는 어떤가?

function foo(){
	const x = 1;
    const y = 2;
    
    function bar() {
    	const z = 3;
        console.log(x);
    }
    return bar;
}

이는 클로저다.

클로저는 이처럼 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지도니느 경우에 한정하는 것이 일반적이다.

이러한 x변수를 '자유 변수'라고 하며 클로저란 이러한 자유변수에 대해 닫혀있다는 의미이다.

메모리 점유 걱정은 하지 않아도 된다. 왜냐하면 모던 자바스크립트 엔진은 최적화가 잘 되어 참조하지 않는 식별자는 기억하지 않기 때문이다.

고차함수 추가

Array.sort()

const ascSort = (arr) =>
    arr.sort((a, b) => {
      if (!isNaN(a) && !isNaN(b)) return a - b; // 둘다 숫자인 경우 값을 비교해서 오름차순 정렬
      // 둘다 문자열인 경우
      if (isNaN(a) && isNaN(b)) {
        // 길이가 다른 경우 길이순 오름차순 정렬
        if (a.length !== b.length) return a.length - b.length;
        else return a.charCodeAt() - b.charCodeAt(); // 길이가 같은 경우 아스키코드 오름차순 정렬
      } else return isNaN(a) - isNaN(b); // 그 외의 경우, 숫자가 앞으로 오도록 정렬
    });

Array.forEach()

// 합집합
  const sum = (baseSet, otherSet) => {
    const sumSet = [...baseSet];
    otherSet.forEach((value) => {
      if (!baseSet.includes(value)) sumSet.push(value);
    });
    return ascSort(sumSet);
  };

Array.some()

// 이동할 수 있는 곳인지 체크
    if (!fromObj.possiblePositions().some((obj) => obj.position === to)) {
      console.log(`${fromObj.type}은 해당 위치로 이동할 수 없습니다.`);
      return false;
    }

Array.every()

const equalArr = (aArr, bArr) => {
  if (aArr.length !== bArr.length) return false;
  return aArr.every((a) => bArr.includes(a)); // 모두 포함되어 있는 경우만 true
};

🎉 기타 공부하며 알게된 것들

Array와 Set 성능 비교

Set객체를 언제 사용해야할까? Array와 성능에서 어떤 차이가 있을까?
스택오버플로우에서 진행된 실험에 따르면(Javascript Set vs. Array performance)

  • <요소 추가> array에 .push를 쓰는 것이 set에 .add를 쓰는 것보다 약 4배 빠르다. (요소를 얼마나 추가하는지는 상관 x)
  • <요소 탐색> for문으로 array를 iterate하는 것이 for...of로 set을 iterate하는 것보다 빨랐다. 심지어 작은 테스트에서는 두배 차이났지만, 큰 테스트에서는 4배 차이가 났다.(여기서 작은 테스트란 1만개, 큰 테스트란 10만개를 다뤘다)
  • <요소 삭제> for문과 .splice를 이용해 array에서 요소를 제거하고, for...of와 .delete를 이용해 set에서 요소를 제거했다. 작은 테스트에서는 set이 array에 비해 세 배 정도 빨랐으나(2.6ms vs 7.1ms), 큰 테스트에서는 무려 23배 차이가 났다. (83.6ms vs 1955.1ms)

결론 : 요소를 제거할 때는 set으로 연산하는 것이 빠르고, 그 외에는 array가 더 빠르다.

Arrow Function은 function을 대체하는 신문법이 아니다?

장점

  • 함수 본연의 기능을 아주 잘 표현한다.
  • 소괄호(파라미터가 하나라면) 및 중괄호(리턴이 한줄이라면) 생략이 가능하다.
<실제 코드 전>
const factors = (number) => {
  const factorArr = Array(number - 1)
    .map((_, i) => i + 1)
    .filter((x) => isFactor(number, x));
  return factorArr;
};
<실제 코드 후>
const factors = number =>
  Array(number - 1)
    .fill()
    .map((_, i) => i + 1)
    .filter((x) => isFactor(number, x));
  • 화살표 함수는 함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정된다. 동적으로 결정되는 일반 함수와는 달리 화살표 함수의 this 언제나 상위 스코프의 this를 가리킨다. 이를 Lexical this라 한다. 이때문에 장점이자 사용해서 안되는 경우가 생긴다.

사용해서는 안되는 경우

메소드

화살표 함수로 메소드를 정의하는 것은 피해야 한다. 화살표 함수로 메소드를 정의하여 보자.

// Bad
const person = {
  name: 'Lee',
  sayHi: () => console.log(`Hi ${this.name}`)
};

person.sayHi(); // Hi undefined

위 예제의 경우, 메소드로 정의한 화살표 함수 내부의 this는 메소드를 소유한 객체, 즉 메소드를 호출한 객체를 가리키지 않고 상위 컨택스트인 전역 객체 window를 가리킨다. 따라서 화살표 함수로 메소드를 정의하는 것은 바람직하지 않다.

이와 같은 경우는 메소드를 위한 단축 표기법인 ES6의 축약 메소드 표현을 사용하는 것이 좋다.

prototype

화살표 함수로 정의된 메소드를 prototype에 할당하는 경우도 동일한 문제가 발생한다. 화살표 함수로 정의된 메소드를 prototype에 할당하여 보자.

// Bad
const person = {
  name: 'Lee',
};

Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);

person.sayHi(); // Hi undefined

화살표 함수로 객체의 메소드를 정의하였을 때와 같은 문제가 발생한다. 따라서 prototype에 메소드를 할당하는 경우, 일반 함수를 할당한다.

생성자 함수

화살표 함수는 생성자 함수로 사용할 수 없다. 생성자 함수는 prototype 프로퍼티를 가지며 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor를 사용한다. 하지만 화살표 함수는 prototype 프로퍼티를 가지고 있지 않다.

const Foo = () => {};

// 화살표 함수는 prototype 프로퍼티가 없다
console.log(Foo.hasOwnProperty('prototype')); // false

const foo = new Foo(); // TypeError: Foo is not a constructor

addEventListener 함수의 콜백 함수

addEventListener 함수의 콜백 함수를 화살표 함수로 정의하면 this가 상위 컨택스트인 전역 객체 window를 가리킨다.

// Bad
const button = document.getElementById('myButton');

button.addEventListener('click', () => {
  console.log(this === window); // => true
  this.innerHTML = 'Clicked button';
});

객체 또는 배열의 깊은 복사

const arrA = [1, 2, 3, 4];
const arrB = arrA;
arrA.push(5);
console.log(arrB); // [ 1, 2, 3, 4, 5 ]
const arrA = [1, 2, 3, 4];
const arrB = [...arrA];
arrA.push(5);
console.log(arrB); // [ 1, 2, 3, 4 ]
const objA = { a: 1, b: 2, c: 3 };
const objB = objA;
objA.a = 2;
console.log(objB); // { a: 2, b: 2, c: 3 }
const objA = { a: 1, b: 2, c: 3 };
const objB = { ...objA };
objA.a = 2;
console.log(objB); // { a: 1, b: 2, c: 3 }
const objA = { a: { d: 4, e: 5 }, b: 2, c: 3 };
const objB = { ...objA };
objA.a.d = 2;
console.log(objB); // { a: { d: 2, e: 5 }, b: 2, c: 3 }
  • 함수형에 파라미터 여러개를 배열로 받을 때도 사용가능
function func(...arg) {
  return arg;
}
console.log(add(1, 2, 3, 4, 5, 6)); //[ 1, 2, 3, 4, 5, 6 ]

😊느낀점

그동안 내가 작성해오던 코드의 느낌이 함수형 프로그래밍이라는 사실을 알게 되었다. 그래서인지 객체지향 프로그래밍을 배우며 어렵고 또 재밌게 느껴졌는지 모르겠다. OOP도 잘 배워서 FP랑 잘 융합하면 좋을 것 같다는 생각을 했다. 그럴려면 OOP를 잘 다루어야겠지. 코딩테스트 준비를 하면서 고차함수들을 이것저것 써보던 것이 도움이 됐다. 오늘은 이론에 대해 많이 알게된 것 같아 뿌듯하다. 구현에 시간을 쏟는 것보다 이론을 알고 그에 맞게 적용하는 것이 훨씬 도움되는 것 같다. 앞으로는 구현과 체크포인트에 너무 얽매이지 말자 화이팅!

출처

profile
Earn this, Earn it.

0개의 댓글