CS와 Node.js 6. JS 함수형 프로그래밍! 함수와 클로저, 순수함수, 불변성, JS 고차 함수, iterable

박재하·2023년 11월 3일
0

CS와 Node.js

목록 보기
6/12

함수형 패러다임

함수와 클로저

PoiemaWeb

변수의 유효범위와 클로저

함수

JS는 대표적인 함수 지향 언어로, 이를 통해 다른 언어와는 달리 개발자에게 많은 자유를 준다. 대표적으로 다음이 가능하다.

  • 함수를 동적으로 생성할 수 있음
  • 생성한 함수를 다른 함수에 인수로 넘길 수 있음(callback?)
  • 생성된 곳이 아닌 다른 곳에서 함수를 호출할 수 있음

클로저

클로저(closure)외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미한다. 다른 언어는 일반적으로 클로저 구현이 불가능하거나 특수한 방식으로 작성해야 하지만, JS는 모든 함수가 자연스럽게 클로저가 됩니다.

JS 내 모든 함수숨김 프로퍼티 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억하며, 함수 본문에서는 이를 이용해 외부 변수에 접근할 수 있습니다.

다음은 JS에서 클로저를 사용하여 합을 구하는 함수 예제입니다.

function sum(a) {
  return function (b) {
    return a + b; // 'a'는 외부 렉시컬 환경에서 가져옵니다.
  };
}

console.log(sum(1)(2)); // 3
console.log(sum(5)(-1)); // 4

간단하지만 함수와 구분되는 클로저의 특징을 잘 알 수 있는 예제입니다.

추가: 함수와 클로저의 정확한 정의

함수 - JavaScript | MDN

클로저 - JavaScript | MDN

또한 MDN(Mozilla Developer Network)은 함수에 대해 아래와 같이 설명한다.

작업을 수행하거나 값을 계산하는 명령문의 집합인 프로시저와 비슷하지만, 프로시저가 함수로 쓰이려면 입력과, 입력과 명확한 관계가 있는 출력을 반환해야 한다. 또한 함수를 호출할 스코프 내에서 함수를 정의해야 한다.

또한 MDN은 클로저에 대해 아래와 같이 정의한다.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)의 조합이다.

렉시컬 환경을 알기 위해서는 Lexical Scoping, 즉 JS가 변수의 유효범위를 어떻게 지정하는지를 이해해야 한다.

  • 함수를 호출할 때가 아닌 함수를 어디에 선언하였는지에 따라 스코프가 결정된다.
  • 따라서 함수를 어디에서 실행하더라도 [[Environment]]에 저장된 해당 함수의 정의 당시 실행 환경(외부 변수 등 포함)에서 실행된다!

순수 함수와 그렇지 않은 함수

변하지 않는 상태를 유지하는 방법, 불변성(Immutable)

순수 함수

수학 개념에서 시작된 순수 함수는 다음과 같은 두 가지 특징을 가진다.

  • 동일한 인풋(인자)에는 항상 동일한 결과를 내야 한다.
  • 함수 외부의 상태를 변경하거나, 외부의 상태에 영향을 받아서는 안된다.

불변성

불변성이란 “상태를 변경하지 않는 것”으로 짧게 정의할 수 있다.

함수형 프로그래밍의 관점에서, 불변성은 “변수에 값을 재할당하지 않는다” 등과 같이 불변성 유지의 조건을 생각해볼 수 있으며, 외부 상태의 영향을 받아 내부의 상태를 변경하거나 내부에서 외부의 상태를 변경하는 일방지해야 하는 순수 함수 개념과도 연관성이 있다.

특히 객체지향 관점과 혼합하면 클래스에서 constructor()를 제외한, 내부에서 클래스 속성에 직접 값을 할당하거나 메소드를 통해 속성을 수정 하는 등의 클래스 불변성을 변경하는 행위를 방지해야 함을 유추할 수 있다!

미션에서 구현한 함수들 중 순수 함수와 그렇지 않은 함수

함수형 프로그래밍으로 모두 개선작업을 거쳤기 때문에, constructor()를 제외하면 모두 순수 함수라고 할 수 있다. 정리하면 다음과 같다.

  • 순수 함수
    • Movie.updateTickets()
    • Movie.updateTheaters()
    • Movie.getMovie()
    • MovieList.add()
    • MovieList.updateTickets()
    • MovieList.updateTheaters()
    • MovieList.delete()
    • MovieList.sortByDate()
    • MovieList.top10Tickets()
    • MovieList.findBy()
    • MovieList.totalTheaters()
    • MovieList.map()
    • MovieList.filter()
    • MovieList.display()
  • 그렇지 않은 함수
    • Movie.constructor()
    • MovieList.constructor()

JS 고차 함수

고차 함수하나 이상의 함수를 인자로 받거나, 함수를 결과로 뱉는 함수를 말함!

Higher Order Functions in JavaScript – Explained with Practical Examples

[JavaScript] 함수 합성(Function Composition)

Object.entries() - JavaScript | MDN

pipe(), compose()

A quick introduction to pipe() and compose() in JavaScript

[JavaScript]pipe 함수와 compose 함수 사용 방법

pipe(), compose()는 여러 함수를 합성하여 하나의 함수처럼 동작하게 해주는 함수를 의미한다. 실제로 제공하는 함수가 아닌 자주 사용되는 구현 패턴이라고 한다.

const pipe =
  (...funcs) =>
  (initVal) =>
    funcs.reduce((val, fn) => fn(val), initVal);
const compose =
  (...funcs) =>
  (initVal) =>
    funcs.reduceRight((val, fn) => fn(val), initVal);

// 아래 코드는 같은 결과를 리턴함
func1(func2(func3(val)));
pipe(func3, func2, func1)(val);
compose(func1, func2, func3)(val);

Array.prototype.some()

전달한 array1와, boolean을 return하는 함수에 대해

하나라도 부함된 조건에 맞으면 true, 모두 부합하지 않으면 false를 리턴한다.

const array1 = [1, 2, 3, 4];

// 하나라도 2의 배수면 true, 모두 2의 배수가 아니면 false
const result = array1.some((currentValue) => {
  return currentValue % 2 === 0;
});

console.log(result);
// true

Array.prototype.every()

some()과는 달리

모두 해당 조건에 맞아야 true, 하나라도 통과하지 않으면 false를 리턴한다.

const array1 = [1, 2, 3, 4];

// 하나라도 2의 배수면 true, 모두 2의 배수가 아니면 false
const result = array1.every((currentValue) => {
  return currentValue % 2 === 0;
});

console.log(result);
// true

Array.prototype.reduce()

Array.prototype.reduce() - JavaScript | MDN

마지막으로는 이해가 잘 되지 않아 본 미션에서 사용하지 못한 reduce()를 소개한다.

const array1 = [1, 2, 3, 4];

// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue
);

console.log(sumWithInitial);
// Expected output: 10

예시와 같이 하나의 값을 리턴한다.

본 함수와 같은 기능을하는 일반적인 명세는 다음과 같다.

const array1 = [1, 2, 3, 4];

// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
let accumulator = initialValue;
for (let i = 0; i < array1.length; i++) {
  accumulator += currentValue;
}
const sumWithInitial = acuumulator;

console.log(sumWithInitial);
// Expected output: 10

객체지향 vs. 함수형

두 패러다임의 가독성

객체지향

  • 코드의 재사용이 가능하다.
  • 분석과 설계의 전환이 쉽다.
  • 객체 간 인과관계를 파악하기 쉽다.

함수형

  • 코드를 짧게 작성할 수 있다.
  • 함수나 객체의 내외부를 분리하여 버그를 찾기 쉽다.

의견

객체지향과 함수형 모두 가독성과 유지보수를 좋게 하기 위한 패러다임으로, 각각의 장점이 있다. 둘 다 차용할 수 있으면 좋을 것 같다!

(예를 들면 객체지향으로 작성하되 내부 메소드나 속성 관리는 함수형 원칙을 준수)

두 패러다임의 공통점과 차이점

공통점

  • 두 패러다임 모두 가독성과 유지보수를 좋게 하기 위해 코드를 어떻게 구성하는지에 대한 논의에서 출발하게 된 것이다.

차이점

객체지향함수형
How, 어떻게 할 것인가에 관심이 있다.What, 무엇을 할 것인가에 관심이 있다.
명령들의 실행 순서가 중요하다.필수적이지 않다.
병렬 코딩이 거의 없다.병렬 코딩 호환이 잘 된다.
흐름 제어가 반복문, 조건문에 따라 행해진다.흐름 제어가 함수 호출과 재귀에 의해 행해진다.
명령형 프로그래밍 모델선언형 프로그래밍 모델
함수가 내외부 스코프에 영향을 미칠 수 있다.함수가 내외부 스코프에 영향을 미칠 수 없다.

의견

상호 배제적인 관계가 아니며, 상호 보완적인 관계라고 할 수 있다!

두 패러다임의 의의를 떠올리며 좋은 코드를 만든다는 공통의 목표로 도달하자

추가 사항

추가적으로, 다른 캠퍼분들을 의견과 해설영상들을 보니 내부적으로도 array 말고 iterable이라는 추상화 레벨이 더 높은 객체를 이용해 map(), reduce(), filter(), flat(), concat(), take() 등의 주요 함수형 프로그래밍 함수들을 만들어서 쓰는 방법이 있다고 한다. 덜덜

직접 만들어봐야 더 이해를 잘 할 테니, 주말에 개인적으로라도 한번 구현해 봐야겠다.

Iterable | JavaScript로 만나는 세상

iterable 객체

클래스를 iterable로 만들기 위해서는 다음과 같이 하면 된다.

How to make an iterator out of an ES6 class

연결 리스트는 이렇게 구현하면 되니까 결국 연결 리스트를 Iterable하게 바꾸고 메소드로 고차함수들 집어넣어놓으면 결국 싹 다 가능

자바스크립트로 연결 리스트를 구현하는 방법

참고 자료

profile
해커 출신 개발자

0개의 댓글