함수형 프로그래밍

Gon Kim·2022년 9월 29일
1

함수형 프로그래밍을 잘 못 이해했을 수 있다. 솔직히 어렵지만, 내 나름대로 이해한 내용을 정리해봤다. 나중에 다시 보고 어떤 부분을 잘 못 알고 있었는지 알 수 있겠지

적용해보려니까 정말 힘들던데, 개념을 대충 알았으니 적용해보고 사고해보려고 노력해야할 것이다.

1. 함수형 프로그래밍

소프트웨어의 크기가 커짐에 따라 복잡하게 엉킨 코드들은 유지보수가 너무 힘들다.

함수형 프로그래밍은 작은 문제를 해결하기 위한 함수를 작성, 사용하기에 가독성이 높고 유지보수가 용이하다고 한다. 왜 그럴까..?

1) 정의

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

함수형 프로그래밍은 다음과 같은 키워드로 설명 가능하다.

2) 선언형 프로그래밍

프로그램 패러다임은 크게 아래와 같이 구분 가능하다.

  • 명령형 프로그래밍 : 무엇을 할지 보다는 어떻게 할 것인지 설명
    • 절차 지향, 객체 지향
  • 선언형 프로그래밍 : 어떻게 보다는 무엇을 할 것인지 설명
    • 함수형 프로그래밍 : 순수 함수를 조합해 소프트웨어를 만드는 방식

솔직히 처음보고 이게 대체 무슨 뜻인가 싶었다. 무엇과 어떻게를 어디에 대입해야할지 상상하기 힘들었다. 좋은 예시가 있더라

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

// 선언형, 함수형, ...
array.map(function(val) {
	return value * 2;
});
  • 명령형의 경우 루프를 돌며 배열의 각 원소에 접근, 접근한 값에 2를 곱해 다시 대입해준다는 ‘어떻게’가 표현되어있다.
  • 선언형의 경우 각 요소에 ‘무엇’을 할 것인지 표현되어있고, 그 처리 방법에 대해서는 코드상에 표현되어있지 않다.

이해가 쏙쏙 되는 예시가 여기도 있다. https://velog.io/@nakta/자바스크립트로-접해보는-함수형-프로그래밍-

3) 순수함수

순수함수 : 입력 값에 대해서만 특정 절차를 거치고, 값을 반환하는 형식의 함수. 함수형 프로그래밍에서 함수들은 순수함수여야한다.

  • 동일한 입력에는 항상 같은 값을 반환한다. → 참조 투명성
  • 함수 내부에서 인자의 값을 변경하거나, 프로그램 상태를 변경하는 side effect가 없다. 따라서 순수함수는 불변성이라는 특성을 지닌다.
    • 또한, 반복문은 재귀함수로 구현된다.

얘도 무슨말인가 싶었다. 아래를 쓱 훑어보자. 이 글을 쓰는 당시 내가 이해한 것을 내 나름의 방식으로 표현해놓은 것도 있다.

Side Effect

side effect는 다음을 일컫는다

  • 변수의 값 변경
  • 자료 구조를 제자리에서 수정
  • 객체의 필드 값을 설정
  • 예외나 오류가 발생하여 실행 중단
  • 콘솔 또는 파일 I/O가 발생

불변성

불변성 : 함수형 프로그래밍에서 함수의 유일한 결과는 함수가 뱉어내는 값일 뿐이다. 다른 부분에는 절대 영향을 미치지도, 받지도 않는다.

  • 기존의 값은 변경되지 않고 유지되어야 하는 것이다.
const person = {name : "me", age : 25};
function increaseAge(person){
	return {...person, age: person.age+1};
}
  • 굳이 데이터의 변경이 필요하면, 원본 데이터 구조는 변경하지 않고, 데이터의 복사본을 만들어 그 일부를 변경, 이를 이용해 작업을 진행한다. 이런 방식으로 인자로 받는 데이터를 변경하지 않을 수 있다.

참조 투명성

참조 투명성 : 함수형 프로그램에서 함수의 결과는 파라미터에만 의존적이다. 함수 외부의 영향을 절대 받지 않는다.

  • x라는 변수에 대해서 무조건 y를 반환한다.
  • 즉 f(x)는 y라는 값으로 대체될 수 있다.

아래 코드는 외부 변수를 참조한다. 참조에 투명하지 않다.

const name = "me";

function hi(){
	console.log(`hey ${name}`);
}

아래와 같이 변경 가능하다.

function hi(name){
	return `hey ${name}`;
}

function main() {
	const h = hi('me');
	console.log(h);
}

이게 대체 무슨 소리일까??

다 읽고 봤을 때, 현실 세계에서 우리가 초중고대학교 생활 내내 접했던, 프로그래밍 세계보다는 우리가 현실 세계에서 많이 접했던 ‘함수’에 가깝다는 느낌이 상당히 강했다. (deterministic한 것만 가정.. 확률적인 요소가 들어가면 참조 투명성에 위배될 것 같다.)

맞는 비유일지는 모르겠지만.. 이해해본 것을 토대로 기술해보겠다.

참조 투명성 부터 보자.

  • 현실 세계에서의 함수를 생각해보자. 정의역과 치역이 존재한다. 함수는 mapping이다. 정의역과 치역 사이의 mapping. 정의역 내의 같은 값에 대해서는 항상 동일한 값이 mapping된다. 이 mapping은 우리가 함수 내부에서 포함하는 것을 제외한 그 어떠한 것에도 영향을 받지 않는다.
  • y = a*x + b라는 함수에서, x라는 input에 대해 y라는 output을 도출하는 과정에 영향을 주는 것은 우리가 함수 내부에서 정의한 a, b외에 없다. 수식을 보라, a, b외에 다른 것이 있는가?

불변성

  • y = a*x + b라는 함수에서, x1라는 input을 넣었다고 해서 정의역에 속해있던 원소가 치역으로 ‘이동’하지 않는다. x1은 근본적으로 정의역에 속하는 원소이다. 함수는 x1라는 input을 가지고서 계산을 하고, y1이라는 결과를 mapping하거나 생성해준다.

순수함수를 설명하며 불변성이 튀어나와 같이 설명했다.

2. 함수형 프로그래밍의 구현 - JS

명령형 프로그램은 if, for, while문으로 흐름을 제어하지만, 함수형 프로그램은 여러 함수를 이어 흐름을 제어한다. 제일 위에서 예시로 보여준 map처럼 개발자는 map이 무엇을 할지를 지정해주거나, map이 어떤 결과를 내어주는 친구인지는 알지만, 내부 동작 원리에 대해 알필요는 없다.

1) First-Class / Higher-Order

First-class: first-class 함수들은 first-class 변수들 처럼 취급된다. first-class 변수들은 함수에 넘겨질 수 있는 파라미터들이다. 즉, first-class 함수들은 파라미터처럼 취급될 수 있는 함수들을 말한다.

Higher-Order: Higher order 함수는 함수를 파라미터로 받는 함수를 의미한다.

뜬금없이 위 단어가 왜 나왔나 싶겠다. JS에서 함수는 first-class이면서 higher-order이다. 함수는 변수처럼 취급될 수 있고, 또 함수의 파라미터로 넘겨질 수 있다.

이 단어 자체가 중요하다기보다는, JS에서 함수를 저렇게 취급하기에 함수형 프로그래밍을 더욱 고차원적으로 하는게 가능해지는 것을 알아두고 넘어가자는 의미에서 소개했다.

2) 체이닝

thisIsObject.removeField('field1').doSth1().doSthElse();

doSthElse(doSth1(removeField(thisIsObject, 'field1')))

위 예시만 봐도 체이닝이라는 단어가 와닿지 싶다.

체이닝은 객체 내에 속한 메서드들을 체인마냥 이어 계속해서 호출해나가는 방법이다. 체이닝을 사용하지 않으면, 아래와 같이 반환 값을 인자로 또 받고, 또 받고, 또 받또받또받고..를 반복해야겠지만, 체이닝을 사용하니 가독성이 좋다.

다만, 객체 내부에 메소드가 정의되어있지 않다면 체이닝으로 사용할 수 없을 것

→ 함수들끼리 강하게 결합되어있는 상태라고 봐도 될 것

lodash 라이브러리에서 체이닝을 지원한다고 한다.

3) 파이프라인

pipe(fn1, fn2, fn3, doSth, ...)(target)

pipe에 함수들을 넣어 파이프라인을 타 내려가듯이 처리해나가는 방법이다. pipe에 아무 함수나 전달해줄 수 있기 때문에 함수들이 느슨하게 결합되어있는 상태라고 봐도 될 것이다

아래와 같은 방법으로 나뉜다. compose는 우측부터, pipe은 좌측부터 시작해 결과를 내뱉는다.

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);
  };
}

4) 커링

이건 진짜 처음 보고 무슨 소리인가 했다. 코드 돌려보고 너무 신기하더라. 어떻게 적용할지는… 아직 모르겠다.

커링은 처리를 유보하는 친구이다. 인자들을 모두 받기 전까지는 함수로 남아있다가, 인자들이 모두 들어오면 결과를 내뱉는다. 이런 아이디어를 누가 내는거지

코드를 보자. 그게 이해가 빠르다

function do(func) {
  return function (x) {
    return function (y) {
      return func(x, y);
    }
  };
}

일단 아이디어 자체만 보고 마구잡이로 구현해보면 위와 같이 구현 가능하다. 그런데 파라미터가 100개이면? ㅋㅋ ㅋ ㅋㅋ.

일반화해서 커링을 사용하는 방법은 구글링하면 나온다. 나도 봤지만 여기에 적어두진 않겠다.

5) 기타

JS에서 함수형 프로그래밍과 관련될만한 기타 개념들을 정리하고 마무리하겠다.

클로저

사실 위 커링 예시는 클로저를 사용한다.

`클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.`

→ 무슨 소리일까? :’)

클로저는 함수가 선언될 때, 외부 변수의 상태 참조하는 경우 그 상태를 그대로 봉합(close)해버린다. 따라서 어떤 일반 함수 내부에서 클로저가 선언되고, 그 일반 함수가 종료되더라도, 일반 함수 내부의 어떤 변수 등을 클로저가 참조했었다면, 계속해서 접근할 수 있게 되는 것이다. 아래 설명이 진짜 짱이다. MDN 짱

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

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

var add5 = makeAdder(5); // add5, add10은 모두 클로저이다.
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값에 접근하여 값을 계산

map, filter 내부 동작

array 자료형의 map, filter 메소드는 다음과 같다.

  • map(callback(currVal [, index [, array]]))
    • callback이 return 하는 것을 모아 새로운 배열로 반환
  • filter(callback(currVal [, index [, array]]))
    • callback 내부 조건문에 부합한 것을 모아 새로운 배열로 반환

이러한 고차함수는 재귀적으로 구현된다. 물론 반복문을 통해서도 원하는 결과를 도출할 수 있지만, 함수형 프로그래밍에서 지향하는 바를 철저하게 지킨다면 재귀적으로 구현해야할 것이다.

참조 투명성, 불변성을 지킨다면 아래 코드와 같이 만들 수 있겠다. 해당 map method는 array내부 메소드가 아닌, 일반 함수라고 가정하겠다. 따라서 array자체도 변수로 받는다.

map(callback, array){
	if(array.length === 0) return [];
	const [firstElement, ...rest] = array;
	return [...map(callback(firstElement), ...rest)];
}

참고

**읽어본 것, 읽어볼 것

https://medium.com/humanscape-tech/함수형-프로그래밍에-관해-7f6172599fc → 간략하고 쉬운 설명

https://www.quora.com/Why-dont-pure-functional-programming-languages-provide-a-loop-construct → 함수형 프로그래밍에서 왜 반복문을 쓰지 않는가?

https://velog.io/@teo/functional-programming → 다시 쓰는 함수형 프로그래밍

https://dev.to/alexmercedcoder/javascript-writing-map-as-a-recursive-function-2854

https://frontsom.tistory.com/10#:~:text=앞에서 말했듯이 커링의,보수 할 때도 유용하다.&text=보통 일반 함수로는,높여보면 다음과 같다 → 커링

https://www.youtube.com/watch?v=cXi_CmZuBgg →functional reactive programming?!

https://www.youtube.com/watch?v=4sO0aWTd3yc → naver d2

**참고한 링크

https://www.geeksforgeeks.org/functional-programming-paradigm/

https://jongminfire.dev/함수형-프로그래밍이란

https://mangkyu.tistory.com/111

https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures 클로저

https://wormwlrm.github.io/2022/03/08/Functional-Programming-in-JavaScript.html, https://velog.io/@nakta/FP-in-JS-자바스크립트로-접해보는-함수형-프로그래밍-함수-컴포지션-커링-s7k2z039vb JS로 접해보는 함수형 프로그래밍을 정리해 놓은 블로그

profile
응애

0개의 댓글