[부스트캠프 웹·모바일 8기] 챌린지 7일차 학습 정리

허지예·2023년 7월 18일
post-thumbnail

함수형(functional) 프로그래밍

함수형 자바스크립트: 모던 웹 개발에 충실한 실전 함수형 프로그래밍 안내서
[함수형 프로그래밍과 ES6+ | 네이버 테크톡]](https://tv.naver.com/v/5328303)

외부에서 관찰 가능한 부수 효과가 제거된 불변 프로그램을 작성하기 위해 순수함수선언적으로 평가하는 것

함수형(functional) 프로그래밍이란?

선언적(declarative) 프로그래밍

함수형 프로그래밍은 선언적 프로그램 패러다임에 속한다. 컴퓨터에게 원하는 작업을 어떻게 수행하는지를 상세히 이르기보다는, 내부적인 제어 흐름이나 상태 변화를 밝히지 않으면서 로직을 표현식(expression)으로 나타낸다.

var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// 명령형, 절차적 프로그래밍
for(let i = 0; i < array.length; i++) {
  array[i] = array[i] * 2;
}

// 함수형 프로그래밍
array.map(function(value) {
  return value * 2;
});

// ES6부터 등장한 화살표 함수
array.map((value) => value * 2);

+) 함수를 인수로 전달하거나 반환하는 함수를 고차 함수(high-order function)이라고 한다.

순수 함수 (Pure Function)

함수형 프로그래밍은 순수 함수로 구성된다.

  • 주어진 입력에만 의존할 뿐, 평가 도중 또는 호출 간 변경될 수 있는 숨겨진 값이나 외부 상태와 무관하게 동작한다.
  • 전역 객체나 레퍼런스로 전달된 매개변수를 수정하는 등 함수 스코프 밖에서 어떠한 변경도 일으키지 않는다.
  • 동일한 입력을 받았을 때 동일한 출력을 반환하는 참조 투명성(referential transparency)을 지닌다.

불변성 (Immutable)

함수형 프로그래밍에서 한 번 선언한 데이터는 불변해야 한다. 원본 변수의 값을 수정하거나 내부 속성을 변경하는 것은 허용되지 않는다.

Javascript에서는 불변 데이터를 흉내내기 위해 const를 쓰거나, 클로저 패턴, Object.writable이나 Object.freeze와 같은 메타 속성을 제어하는 방식을 사용할 수 있다.

제어 흐름 (flow control)

애플리케이션이 정답을 도출하는 데까지 거치는 경로를 제어 흐름(flow control)이라고 한다.

명령형 프로그래밍은 if문을 활용한 분기와 for, while등의 루프로 제어하는 반면, 함수형 프로그래밍은 연속된 블랙박스 연산을 제어하는 방식을 쓴다.

체이닝 (chaining)

객체에 속한 여러 메서드를 단일 구문으로 연쇄 호출하는 객체 지향 패턴을 체이닝(chaining)이라고 한다.

단순 중첩 함수로만 작성된 코드는 가장 안쪽에 감싸진 함수부터 한꺼풀씩 벗겨내는 방식으로 읽어야 해서 가독성이 훨씬 떨어진다.

// 스트링 객체에 속한 메서드 체이닝
'Functional Programming'.toUpperCase().split('').reverse().join('');

// 만약 그냥 함수형로 썼다면 이렇게 됐을 걸...
join(reverse(split(toUpperCase('Functional Programming'), '')), '');

체이닝된 메소드를 수행할 때마다 반환값으로 결과물이 담긴 새 객체를 반환하기 때문에 이러한 연속 호출이 가능하다.

파이프라인 (pipeline)

체이닝 기법은 구조적으로 짜임새가 있고 가독성이 좋아진다. 하지만 객체에 값이 얽매여있기 때문에, 체이닝에서 실행 가능한 메서드 종류에 한계가 존재하기 때문에 코드 표현성이 줄어든다는 한계점도 존재한다.

파이프라인(pipeline)은 함수를 연결하는 또 다른 기법으로, 한 함수의 출력이 다른 함수의 입력이 되게끔 느슨하게 배열한 방향성이 있는 함수 순차열을 말한다.

각 단계를 이루는 함수를 일련의 순서로 합성하는 것이 파이프라인의 기본이다.

// compose는 오른쪽 인자부터 출발하여 차곡차곡 결과값을 쌓아갑니다.
function compose(...functions) {
  return function(arg) {
    return functions.reduceRight((composed, f) => f(composed), arg);
  };
}

// pipe는 왼쪽 인자부터 출발하여 차곡차곡 결과값을 쌓아갑니다.
function pipe(...functions) {
  return function(arg) {
    return functions.reduce((composed, f) => f(composed), arg);
  };
}

compose(
  // ...
  reverse,
  toUpperCase,
)('Functional Programming');

커링 (currying)

커링(currying)은 다변수 함수가 인수를 받을 때까지 실행을 보류 또는 지연시켜 단계별로 나뉜 단항함수의 순차열로 전환하는 기법이다.

Javascript에서는 커링 구현을 클로저 패턴을 위해 사용한다.

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

// 일반적인 커링
function do(func) {
  return function (x) {
    return function (y) {
      return func(x, y);
    }
  };
}

// 화살표 함수를 쓰면 이렇게도 가능합니다.
const do = (func) => (x) => (y) => func(x, y);

do(add)(1)(2)

커링으로 함수를 만들게되면 모자란 인수가 채워지기를 기다리는 새로운 함수가 반환된다. 마지막 인수가 들어오기까지 전체 값을 구하지는 않기 때문에 부분적으로 느긋한 계산(lazy evaluation)이 가능하다.

필요에 따라서는 커링에 필요한 인수 일부를 미리 채울 수도 있는데, 이는 부분 적용 함수(partial applied function)라고 부른다. 이를 이용해 함수의 인수를 일부만 미리 평가할 수 있을 뿐만 아니라 함수 팩토리를 모방할 수도 있다.

함수자와 모나드

함수자는 어떤 불완전한 값을 감싼느 컨테이너 역할을 하는 자료 구조이다. 함수자 안에 값을 집어넣는다는 것을 리프팅이라고 부른다.

함수자는 외부 함수를 인자로 받아 현재 리프트된 값을 적용한 결과값을 다시 새 함수자로 반환하는 매핑 기능을 제공한다.

함수자가 이런 식으로 값을 감싸는 이유는 부수 효과가 있는 모든 함수를 안전하게 순수 함수로 만들기 위한 목적이다.

모나드는 다양한 함수자가 중첩되는 경우를 깔끔하게 연결하기 위해 고안되었다. 즉, 함수자를 연결할 수 있게 만든 함수형 프로그래밍의 디자인 패턴이다.

함수 실행 도중에 예외가 발생한다고 해도, 해당 예외는 함수자라는 컨테이너에 싸여 있기 때문에 예외를 따르면서 남은 함수들을 실행시킨다. 이 모나드 덕에 함수 간 데이터가 안전하게 흘러가도록 조정하고 역할 자원을 추상화할 수 있다.

+) 모나드

안전한 함수 합성을 위해서 사용한다.

배열도 모나드, Promise도 모나드

const g = a => a + 1;
const f = a => a * a;

console.log(f(g(1));
[1].map(g).map(f).forEach(a => console.log(a));

Promise.resolve(1).then(g).then(f).then(a => console.log(a));

여기서 [1]Promise가 모나드. 함수를 안전하게 합성하기 위한 목적을 가지고 있는데, Promise는 비동기적으로 언제 일이 끝날지 모르는 시점을 기다렸다가 합성하려는 성질.

const g = JSON.parse;
const f = ({k}) => k;

const fg = x => Promise.resolve(x)
	.then(g)
	.then(f);

fg('{"K": 10}').then(log);
fg('{"K: 10}').catch(_ => '미안...').then(log);

Promise는 값으로 다루기 위해서 사용하는 것이다.
어떤 일이 일어날지모르는 효과를 감싸놓고, 나는 이런 형태의 값이야라고 말을 해놓고, Promise는 비동기적인 상황과 성공과 실패를 값으로 다루는 형태의 모나드이다.

이다.

// delay
const delay = (time, a) => new Promise(resolve => 
   setTimeOut(() => resolve(a), time));

delay(100, 5).then(log);

// go1
const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);

const a = 10;
const b = delay(1000, 5);

go1(a, log);
go1(b, log);

추가 학습

클로저

클로저 | MDN

 	function makeFunc() {
      var name = "Mozilla";
      function displayName() {
        alert(name);
      }
      return displayName;
    }

    var myFunc = makeFunc();
    //myFunc변수에 displayName을 리턴함
    //유효범위의 어휘적 환경을 유지
    myFunc();
    //리턴된 displayName 함수를 실행(name 변수에 접근)
    function makeAdder(x) {
      var y = 1;
      return function(z) {
        y = 100;
        return x + y + z;
      };
    }

    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    //클로저에 x와 y의 환경이 저장됨

    console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
    console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
    //함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

고차 함수

📚 JavaScript 배열 고차 함수 총정리

.forEach()

  • for문을 대체하는 고차함수.
  • 반복문을 추상화하여 구현된 메서드이고, 내부에서 주어진 배열을 순회하면서 연산을 수행한다.

.map()

  • forEach 같이 순회하면서, 콜백함수에서의 실행결과를 리턴한 값으로 이루어진 배열을 만들어 반환

.find()

  • indexOf() 가 찾고자 하는 값을 인덱스로 주는거고, include()가 찾고자 하는 값을 Bool로 주는거면, find()는 찾고자 하는 값을 그대로 반환한다.
  • 주어진 배열을 순회하면서 콜백 함수 실행의 반환값이 true에 해당하는 첫번째 요소를 반환

.findIndex()

  • 배열 메소드 indexOf() 의 콜백함수 버젼.
  • 고차함수 find()의 리턴값이 인덱스인 버젼.

.filter()

  • 주어진 배열을 순회하면서 콜백 함수의 반환값이 true에 해당하는 요소로만 구성된 새로운 배열을 생성하여 반환.
  • 한마디로 find()의 찾아서 값을 반환하는 기능과 map()의 배열 생성 기능의 융합 버젼.

.reduce()

  • 콜백 함수의 실행된 반환값(initialValue)을 전달 받아 연산의 결과값이 반환.
  • 첫번째 인자(accumulator)서부터 시작해서 배열값인 두번째 인자(currentvalue) 을 순회하며 accumulator+=currentvalue 을 실행.
  • 사실상 forEach, map, filter기능을 reduce로 모두 구현해서 쓸순 있어 고차함수의 부모라고 불림

.sort()

  • 배열 정렬.
  • 단, 복사본이 만들어지는게 아니라 원 배열이 정렬됨.
  • 콜백 함수를 통해 배열의 원소들을 어느 기준으로 정렬할지 지정해야함 (번거로움)

.some()

  • 배열 메소드인 include()의 콜백함수 버전.
  • include는 값이 있냐에 따른 bool이면, some은 함수의 로직에 따른 bool.
  • 배열의 요소들을 주어진 함수(조건)을 통과하는데 한개라도 통과되면 true, 아닐때에는 false를 출력.
  • 빈 배열로 함수(조건)을 통과하면 무조건 false를 출력.
  • 이와같이 some이라는 이름은, 함수(조건)에 부합한 갯수가 some이면 true라는 뜻에서 비롯됨.

.every()

  • some() 의 반대 버전
  • 배열안의 모든 요소가 주어진 함수(조건)을 모두 통과하면 true, 한 요소라도 통과하지 못하면 false를 출력.
  • 빈 배열을 함수에 적용시키면 무조건 true를 반환.
  • 이와같이 every이라는 이름은, 함수(조건)에 부합한 갯수가 every이면 true라는 뜻에서 비롯됨.

객체지향 vs 함수형

객체 지향 프로그래밍은 객체를 중심으로 사고하고 프로그램을 작성하는 것이다.

함수형 프로그래밍은 데이터를 함수로 연결하는 것을 중심으로 사고하고 프로그래밍을 하는 것이다.

함수형 프로그래밍은 객체 지향 프로그래밍을 더 단순하게 그리고 간결하게 보도록 도와준다. 하지만 함수형 프로그래밍이 객체지향보다 반드시 더 나은 것은 아니다.

자바스크립트는 함수형 프로그래밍 기반 위에 객체 지향 언어의 껍데기를 씌운 언어이다. 이렇게 다소 실험적으로 탄생한 이 언어로 인해 개발자들은 '객체지향에 함수형 프로그래밍을 적당히 섞으면 훨씬 더 좋다'라는 사실을 알게 되었다.

profile
대학생에서 취준생으로 진화했다가 지금은 풀스택 개발자로 2차 진화함

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

좋은 글 잘 읽었습니다, 감사합니다.

답글 달기