함수형 프로그래밍이란 무엇을까?
내가 이해한 바로는 순수 함수들을 조합하여 프로그래밍하는 방법으로,
명령형이 아닌 선언형 프로그래밍 방식을 따르고 있다.
순수함수들은 오직 출력(return)만 수행하기 때문에, 입력된 값들이 변경되지 않는다.
따라서 최종 Output이 발생할 수 있도록 순수 함수들을 엮어서 호출해야 한다.
참고로,
명령형은 어떻게 풀어내는지(how to solve)
에 집중하고,
선언형은 무엇을 풀어내는지(what to solve)
에 집중한다.
예를 들어, 최종적인 목표가 '물을 마시기' 라고 해보자.
(명령형) 어떻게(how) 할 것인지를 설명
- 자리에서 일어난다.
- 부엌으로 이동한다.
- 컵과 물을 찾는다.
- 컵에 물을 부어 마신다.
(선언형) 무엇(what)을 할 것인지를 설명
- 물을 마셔보자.
함수형 프로그래밍
은 왜 필요한 걸까?
(결국은 코딩을 어떻게 하면 잘 할 수 있을까? 라는 질문에서 출발한다.)
간단한 과제를 작성하더라도 코드 100줄은 쉽게 넘길 수 있다.
그 100줄 안에 담긴 변수, 함수 들이 서로 연관되어 있다면,
에러가 하나일지라도 100줄의 코드를 다 봐야되는 상황이 발생할 수 있다.
더 무서운 건, 100줄의 코드를 다 봐서 에러를 고치더라도
왜 에러가 고쳐진건지, 다른 에러는 없는지 내가 작성한 코드에 대한 신뢰성이 낮을 수 있다.
이러한 문제를 해결하기 위한 방법 중 하나가 함수형 프로그래밍이다.
Input이 같으면 Output이 일정한 순수함수로 코딩을 했기 때문에
예상하지 못한 Output이 나왔을 때 Input값만 점검하면 된다.
그 Input값 역시 다른 순수함수의 Output이라면, 그 순수함수를 확인해보면 된다.
전체의 코드를 모두 점검할 필요가 없다는 뜻이다.
즉, 개발을 어떻게 하면 효율적으로 잘 할 수 있을지에 대한 의문에서 출발했을 때,
"순수함수를 조합하여 프로그래밍해보니 좋더라!"
라는 결론때문에 "함수형 프로그래밍" 방법을 적용하라는 것이다.
(OOP 패러다임이 나온 것 처럼,
개발 기간과 비용을 단축하여 효율적인 개발을 할 수 있는 프로그래밍 방법이라고 이해했다.)
함수형 프로그래밍에서 가장 중요한 keyword가 아닐까?
함수형 프로그래밍에서는 일반적인 함수
의 조합이 아니라, 순수함수
의 조합이 포인트다.
재사용이 가능한 순수 함수를 원하는 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님 블로그에 이런 정의가 있었다.(출처)
즉, 절차적 프로그래밍
에서는 Input
이 절차(=흐름)대로
흘러가서
내가 원하는 Output
으로 변경
이 되어 최종 출력
이 되는 반면,
함수형 프로그래밍
에서는 "Input
이 들어왔을 때 내가 원하는 값
은 무엇('what')
이다." 라고
정의한 순수함수
들을 조합(연속 혹은 병렬로 호출)
하여 최종 Output을 출력한다.
함수형 프로그래밍의 <순수함수 조합(Composion)>이
절차적프로그래밍처럼 <절차와 흐름>이라는 특징은 있을 수 있다.
가장 큰 차이점은!
<절차형>은상태
를변화
시키고, (➲ Side Effect 존재)
<함수형>은상태
를변화시키지 않는다
. (➲ Side Effect 존재하지 않는다.)
위키백과에도 이렇게 설명한다.
명령형은 <상태를 바꾸는 것>을 강조하는 것과는 달리,
함수형 프로그래밍은 <함수의 응용>을 강조한다.
물론 현실에서 완전한 순수함수를 구현하는 것은 힘들 것이다.
(Side Effect가 없다...... 0.....이라니...상상도 안된다.)
어쩌면 함수형 프로그래밍은 개발자가 Side Effect도 예측해서 컨트롤이 가능한 단계가 아닐까?;
(물론 Side Effect가 발생하도록 설계하면 순수함수를 구현했다고 할 수는 없겠지만!)
객체지향 프로그래밍과 함수형 프로그래밍, 둘을 어떻게 해석해야할까?
(누가 더 좋은거지? 어떤 차이가 있지? 등등)
둘다 프로그래밍의 패러다임에서 개발자가 ‘지향’ 해야하는 방법이라고 설명한다.
(강의에서도 따로 쓸수도 있고, 같이 쓸수도 있고 상황에 따라 다른 것이라 했다.)
(이해가 되지 않을땐...구글링이 최고다... 그러다 몇가지 글을 발견했다.)
출처
명령적 프로그래밍은 상태 공유에 따른 부작용이 극대화되는 문제가 있으며, 따라서 구조적 구성을 통해 상태가 공유되는 범위를 제한(함수나 객체와 같이) 하는 노력이 계속되었다.
객체 지향의 패러다임은 클래스라는 타입에 맞춰 공유 상태를 분류하고, 객체라는 경계에 따라 공유 범위를 제한한다. 비록 객체의 상태는 시스템에 공유될 수 있지만, 일부 상태는 객체 외부로부터 숨길 수 있으며, 모듈화 된 그릇에 상태를 모아 관리하기 때문에 명력적 프로그래밍에 비해 상당히 구조적으로 변화한 형태다.
함수형 프로그래밍은 여기서 더 나아가 공유 상태를 함수범위(객체 범위보다 좁게)로 제한하는 방향을 지향한다. 결국 순수한 함수형 언어에선 상태가 함수 밖으로 드러나지 않으며, 함수는 Input/Output만을 제공하고 상태의 측면에서는 완전한 블랙박스가 된다. 따라서 프래그래밍의 구조화 정도는 객체 지향적 개념보다 함수형 프로그램이 더 높아 보인다.
출처
명령형 프로그래밍(Imperative programming) 에서 멀티스레드를 활용한 동시성 프로그래밍은 개발자들을 아주 힘들게 하는 것 중 하나다. .....
명령형 프로그래밍에서 이러한 문제가 발생하는 주 원인은 스레드 간에 공유되는 데이터나 상태 값이 변경 가능(mutable)하기 때문이다. 하지만, 함수형 프로그래밍에서는 사용하는 모든 데이터가 변경 불가능(immutable)하고 함수는 부수 효과를 가지고 있지 않다. 때문에, 여러 스레드가 동시에 공유 데이터에 접근하더라도 해당 데이터가 변경될 수 없기 때문에 동시성과 관련된 문제를 원천적으로 봉쇄한다
내가 이해한 바로는...
상태 변화
를 통해 원하는 최종값을 얻어내는 명령적 프로그래밍
은,
절차적, 구조적, 객체지향
순으로 발전했다.
그러나 명령적 프로그래밍
은 상태 변화
로 인해 side effect
가 존재한다.
이를 개선하기 위한 방법으로, 선언적 프로그래밍
이 제안되었다.
함수형 프로그래밍
은 순수함수의 조합으로 선언적 프로그래밍
Rule을 준수한다.
따라서 명령형 프로그램의 단점인 side effect
를 개선
할 수 있다.
즉,
☛👉 객체지향 프로그래밍 ➲ 상태 변화 ➲ Side effect 발생
☛👉 함수형 프로그래밍 ➲ 상태 변화 없음 ➲ Side effect 미발생
(결국 명령형이냐, 선언형이냐 차이처럼 느껴졌다)
물론 side effect가 없다고 해서 객체지향 프로그래밍보다 함수형 프로그래밍만 써야하는 건 아니다.
함수형 프로그래밍도 단점은 있다.
따라서 정말 강의에서 얘기했던 것처럼,
객체지향 프로그래밍과 함수형 프로그래밍은 상황에 따라 같이도, 다르게도 쓸 수 있을 것이다.
함수형 프로그래밍에서 Immutabiltity도 중요한 개념이다.
이쯤에서 shallow copy
와 deep 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을 지키지 못하면, 다른 순수함수에도 영향을 미친다.)
//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); //이렇게붙어요!