함수형 프로그래밍이란 무엇을까?

정의

  • 함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고,
    상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
  • 문이 아닌 식이나 선언으로 수행되는 선언형 프로그래밍 패러다임을 따르고 있다.
    출처:위키백과

내가 이해한 바로는 순수 함수들을 조합하여 프로그래밍하는 방법으로,
명령형이 아닌 선언형 프로그래밍 방식을 따르고 있다.
순수함수들은 오직 출력(return)만 수행하기 때문에, 입력된 값들이 변경되지 않는다.
따라서 최종 Output이 발생할 수 있도록 순수 함수들을 엮어서 호출해야 한다.

참고로,
명령형은 어떻게 풀어내는지(how to solve)에 집중하고,
선언형은 무엇을 풀어내는지(what to solve)에 집중한다.

예를 들어, 최종적인 목표가 '물을 마시기' 라고 해보자.

(명령형) 어떻게(how) 할 것인지를 설명
 - 자리에서 일어난다.
 - 부엌으로 이동한다.
 - 컵과 물을 찾는다.
 - 컵에 물을 부어 마신다.
 
 (선언형) 무엇(what)을 할 것인지를 설명
 - 물을 마셔보자.

함수형 프로그래밍은 왜 필요한 걸까?
(결국은 코딩을 어떻게 하면 잘 할 수 있을까? 라는 질문에서 출발한다.)

간단한 과제를 작성하더라도 코드 100줄은 쉽게 넘길 수 있다.
그 100줄 안에 담긴 변수, 함수 들이 서로 연관되어 있다면,
에러가 하나일지라도 100줄의 코드를 다 봐야되는 상황이 발생할 수 있다.
더 무서운 건, 100줄의 코드를 다 봐서 에러를 고치더라도
왜 에러가 고쳐진건지, 다른 에러는 없는지 내가 작성한 코드에 대한 신뢰성이 낮을 수 있다.

이러한 문제를 해결하기 위한 방법 중 하나가 함수형 프로그래밍이다.

Input이 같으면 Output이 일정한 순수함수로 코딩을 했기 때문에
예상하지 못한 Output이 나왔을 때 Input값만 점검하면 된다.
그 Input값 역시 다른 순수함수의 Output이라면, 그 순수함수를 확인해보면 된다.
전체의 코드를 모두 점검할 필요가 없다는 뜻이다.

즉, 개발을 어떻게 하면 효율적으로 잘 할 수 있을지에 대한 의문에서 출발했을 때,
"순수함수를 조합하여 프로그래밍해보니 좋더라!"
라는 결론때문에 "함수형 프로그래밍" 방법을 적용하라는 것이다.

(OOP 패러다임이 나온 것 처럼,
개발 기간과 비용을 단축하여 효율적인 개발을 할 수 있는 프로그래밍 방법이라고 이해했다.
)

함수형 프로그래밍의 특징

1) 순수함수(Pure function)

함수형 프로그래밍에서 가장 중요한 keyword가 아닐까?
함수형 프로그래밍에서는 일반적인 함수의 조합이 아니라, 순수함수의 조합이 포인트다.

  • 동일한 입력에는 항상 같은 값을 반환해야 한다.
  • 함수의 출력(return)은 오로지 그 함수에 입력된 값(input)에만 의존한다.
  • 함수의 실행은 프로그램의 실행에 영향을 미치지 않아야 한다.
    (Self-contained되어야 한다, 즉, side effect가 없다.)
    • Side effect가 없다는 의미는 오로지 출력(return) 만 수행한다는 의미이다.

2) Function composition

  • 둘 이상의 함수를 조합하는 과정을 말한다.
  • 함수형 프로그램은 여러 작은 순수 함수들로 이루어져있기 때문에
    이 함수들을 연쇄적으로 또는 병렬로 호출해서 더 큰 함수를 만드는 과정으로
    전체 프로그램을 구축해야 한다.

재사용이 가능한 순수 함수를 원하는 Output을 위해 서로 엮는 것이다.
함수를 엮기 때문에 고차원함수(Higher-Order Functions)를 활용해야 한다.

고차원 함수(Higher-Order Functions)

  • 함수를 인자(argument)로 받는다.
  • 함수를 결과로 반환한다.

함수형 프로그래밍에서는 함수가 1등급 객체(first-class object)이다.
함수가 1등급 객체이므로, 함수는 고차원 함수를 구현할 수 있다.

1등급 객체(first-class object (citizen, type, entity, value로 표현되기도 한다.)

  • 자료구조에 저장된다.
  • 인자(argument)로 전달된다.
  • 결과로 반환한다.
  • 함수가 실행되는 동안 새로운 함수가 만들어질 수 있다.

# 함수형 프로그래밍은 절차적 프로그래밍과 같다?

함수형 프로그래밍을 공부 하다보니 절차적 프로그래밍 방법론이 떠올랐다.

함수형 프로그래밍도 결국 순수함수들을 실행해서 Output을 만들어낸다.
Input에서 Output의 흐름대로 프로그래밍하는 절차적 프로그래밍과 유사하다는 생각이 들었다.
절차적, 구조적, 객체지향으로 발전해온 프로그래맹 방법론이 역행한 것 같은 느낌이었다.

또 함수형 프로그래밍에서는 함수의 실행순서가 중요하지 않다는 얘기가 있었다.
그러나 내가 봤을 땐 함수형 프로그래밍도 실행 순서가 있어야 원하는 최종 출력값을 얻는 것 아닌가 라는 생각이 들었다.
(오로지 출력만 하는 순수함수들을 순서대로 엮지 않는다면, 또 순서도 중요하지 않다면
무엇을 만들어 내는것인가...
)

Kyusung님 블로그에 이런 정의가 있었다.(출처)

  • 명령형 프로그래밍: 프로그래밍의 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하는 방식
    • 절차지향 프로그래밍: 수행되어야 할 연속적인 계산 과정을 포함하는 방식 (C, C++)
    • 객체지향 프로그래밍: 객체들의 집합으로 프로그램의 상호작용을 표현 (C++, Java, C#)
  • 선언형 프로그래밍: 어떤 방법으로 해야 하는지(How)를 나타내기보다 무엇(What)과 같은지를 설명하는 방식
    • 함수형 프로그래밍: 순수 함수를 조합하고 소프트웨어를 만드는 방식 (클로저, 하스켈, 리스프)

즉, 절차적 프로그래밍에서는 Input절차(=흐름)대로 흘러가서
내가 원하는 Output으로 변경이 되어 최종 출력이 되는 반면,
함수형 프로그래밍에서는 "Input이 들어왔을 때 내가 원하는 값무엇('what')이다." 라고
정의한 순수함수들을 조합(연속 혹은 병렬로 호출)하여 최종 Output을 출력한다.

함수형 프로그래밍의 <순수함수 조합(Composion)>이
절차적프로그래밍처럼 <절차와 흐름>이라는 특징은 있을 수 있다.

가장 큰 차이점은!
<절차형>은 상태변화시키고, (➲ Side Effect 존재)
<함수형>은 상태변화시키지 않는다. (➲ Side Effect 존재하지 않는다.)

위키백과에도 이렇게 설명한다.

명령형은 <상태를 바꾸는 것>을 강조하는 것과는 달리,
함수형 프로그래밍은 <함수의 응용>을 강조한다. 

물론 현실에서 완전한 순수함수를 구현하는 것은 힘들 것이다.
(Side Effect가 없다...... 0.....이라니...상상도 안된다.)
어쩌면 함수형 프로그래밍은 개발자가 Side Effect도 예측해서 컨트롤이 가능한 단계가 아닐까?;
(물론 Side Effect가 발생하도록 설계하면 순수함수를 구현했다고 할 수는 없겠지만!)

# 객제지향 프로그래밍 vs 함수형 프로그래밍

객체지향 프로그래밍과 함수형 프로그래밍, 둘을 어떻게 해석해야할까?
(누가 더 좋은거지? 어떤 차이가 있지? 등등)
둘다 프로그래밍의 패러다임에서 개발자가 ‘지향’ 해야하는 방법이라고 설명한다.
(강의에서도 따로 쓸수도 있고, 같이 쓸수도 있고 상황에 따라 다른 것이라 했다.)

(이해가 되지 않을땐...구글링이 최고다... 그러다 몇가지 글을 발견했다.)

출처
명령적 프로그래밍은 상태 공유에 따른 부작용이 극대화되는 문제가 있으며, 따라서 구조적 구성을 통해 상태가 공유되는 범위를 제한(함수나 객체와 같이) 하는 노력이 계속되었다.
객체 지향의 패러다임은 클래스라는 타입에 맞춰 공유 상태를 분류하고, 객체라는 경계에 따라 공유 범위를 제한한다. 비록 객체의 상태는 시스템에 공유될 수 있지만, 일부 상태는 객체 외부로부터 숨길 수 있으며, 모듈화 된 그릇에 상태를 모아 관리하기 때문에 명력적 프로그래밍에 비해 상당히 구조적으로 변화한 형태다.
함수형 프로그래밍은 여기서 더 나아가 공유 상태를 함수범위(객체 범위보다 좁게)로 제한하는 방향을 지향한다. 결국 순수한 함수형 언어에선 상태가 함수 밖으로 드러나지 않으며, 함수는 Input/Output만을 제공하고 상태의 측면에서는 완전한 블랙박스가 된다. 따라서 프래그래밍의 구조화 정도는 객체 지향적 개념보다 함수형 프로그램이 더 높아 보인다.

출처
명령형 프로그래밍(Imperative programming) 에서 멀티스레드를 활용한 동시성 프로그래밍은 개발자들을 아주 힘들게 하는 것 중 하나다. .....
명령형 프로그래밍에서 이러한 문제가 발생하는 주 원인은 스레드 간에 공유되는 데이터나 상태 값이 변경 가능(mutable)하기 때문이다. 하지만, 함수형 프로그래밍에서는 사용하는 모든 데이터가 변경 불가능(immutable)하고 함수는 부수 효과를 가지고 있지 않다. 때문에, 여러 스레드가 동시에 공유 데이터에 접근하더라도 해당 데이터가 변경될 수 없기 때문에 동시성과 관련된 문제를 원천적으로 봉쇄한다

내가 이해한 바로는...
상태 변화를 통해 원하는 최종값을 얻어내는 명령적 프로그래밍은,
절차적, 구조적, 객체지향 순으로 발전했다.
그러나 명령적 프로그래밍상태 변화로 인해 side effect가 존재한다.
이를 개선하기 위한 방법으로, 선언적 프로그래밍이 제안되었다.
함수형 프로그래밍은 순수함수의 조합으로 선언적 프로그래밍 Rule을 준수한다.
따라서 명령형 프로그램의 단점인 side effect개선할 수 있다.

즉,
☛👉 객체지향 프로그래밍 ➲ 상태 변화 ➲ Side effect 발생
☛👉 함수형 프로그래밍 ➲ 상태 변화 없음 ➲ Side effect 미발생
(결국 명령형이냐, 선언형이냐 차이처럼 느껴졌다)

물론 side effect가 없다고 해서 객체지향 프로그래밍보다 함수형 프로그래밍만 써야하는 건 아니다.
함수형 프로그래밍도 단점은 있다.

  • 순수함수를 구현하기 위해 코드의 가독성이 좋지 않을 수 있다.
  • 함수형 프로그래밍에서는 반복을 for문이 아닌 재귀를 통해 이뤄지는데,(=deep copy)
    재귀적 코드 스타일은 무한루프에 빠질 수 있다.
  • 순수함수를 쓰는건 쉬울 수 있지만, 그것들을 조합하는 것은 쉽지 않다.

따라서 정말 강의에서 얘기했던 것처럼,
객체지향 프로그래밍과 함수형 프로그래밍은 상황에 따라 같이도, 다르게도 쓸 수 있을 것이다.

3) Immutability(불변성)

함수형 프로그래밍에서 Immutabiltity도 중요한 개념이다.

  • <Input(입력값)의 Immutability>라는 의미는 <Input이 stable 해야 한다>는 것이다.
    • 순수함수는 예측된 input과 함께 실행된다.
    • Input이 변하면, 순수함수는 개발자가 예측하지 않은 다른 출력값을 만든다.
    • 즉, 다른 출력값은 다른 함수에도 영향을 미친다.
  • 순수함수는 input이 변경되지 않도록, 또는 input이 immutable하도록
    가능한 self-contained 되도록 설계되어야 한다.

# shallow copy vs deep copy

이쯤에서 shallow copydeep copy란 개념이 간혹 검색된다.

Input이 변경되지 않도록 순수함수에서는 input을 copy해서 사용하는데,
call by value가 적용되는 원시 타입(privitive type)이면 문제가 없지만,
call by reference가 적용되는 참조 타입(reference type)이면 얘기가 좀 달라진다.

객체처럼 참조타입 데이터는 해당 값이 아닌 '값이 저장된 메모리의 주소'가 저장된다.
Shallow copy(얕은 복사)는 참조형 Type 데이터가 저장한 '메모리 주소 값'복사한 것을 의미한다.
반대로 Deep copy새로운 메모리 공간확보완전히 복사하는 것을 의미한다.

//shallow copy에 대해 간단하게 코드로 확인해보자.
var list = ["a", "b", ["c"]];
var listCopy = list.slice(); //list를 복사했다.
listCopy[2].push("d"); //listCopy만 변경해보았다.
console.log(list); //["a", "b", ["c", "d"]];   -> 원본까지 바뀐다!!
console.log(listCopy); //["a", "b", ["c", "d"]];                 

☛👉 만약 순수함수의 Input이 객체, 배열 같은 참조형 Type이라면,
shallow copy로 인해 원본이 변경되지 않도록 주의해야 한다.
(원본의 immutability을 지키지 못하면, 다른 순수함수에도 영향을 미친다.)

4) Function decoration

  • 순수함수를 재사용하다보면, 이미 만들어진 순수함수를 그대로 적용하기보다
    일부 개선하거나, 조금 다른 방향으로 수정해야 하는 상황이 발생할 수 있다.
    이 때 function decoration을 적용한다.
  • 직접적으로 순수 함수를 수정하진 않지만, 함수에 실행 시점이나 방식을 조율하는 것을 말한다.
    (ex, memorize, debounce)

5) Arity Mismatch (w. Partial Application, Curring)

  • 순수 함수를 조합하다보면, 인자의 수가 서로 맞지 않는 순수함수를 엮어야 하는 상황이 있을 수 있다.
    이 상황을 Arity Mismatch라 한다.
    • 함수는 무조건 1개의 return 값만 나온다.
    • 순수함수 중에는 2개 이상의 인자를 필요로 하는 함수가 있을 수 있다.
    • 따라서 1개의 순수함수가 아닌 2개 이상의 순수함수에서 실행된 출력값을 받아와야하는데,
      이렇게 인자의 수가 서로 맞지 않는 상황을 Arity Mismatch라 한다.
  • Partial Application, Curring으로 Arity Mismatch 상황을 해결할 수 있다.
    • Partial Application : 인자를 부분적으로 먼저 엮어준다.
    • Curring : 인자를 하나씩만 받는 함수의 체인으로 만드는 방법이다.
//Partial (예시는 Lodash _.partial이다.) [출처](https://marpple.github.io/partial.js/)
function add(a, b, c) {
  return a + b + c;
}
var addTenFive = _.partial(add, 10, 5); //10과 5를 미리 합쳐준다!
console.log(addTenFive(5)); // 미리 10과 5가 합쳐줬기 때문에 20이 나온다.
//Curring
function before(a) {
  return function after(b) {
  	return a + b;
  }
}
var word = before("이렇게");
var finalWord = word("붙어요!");
console.log(finalWord); //이렇게붙어요!
  

참고

profile
꾸준히, 끄적끄적 해볼게요 :)

0개의 댓글