상위 문서: 프로그래밍 패러다임
1. 개요
파일:external/img.viralpatel.net/xkcd-functional.png함수가 대단히 특별하지 않은 언어를 훵셔널 랭귀지라 한답니다.
이광근[1]
이 문서는 함수형 언어를 설명한다.이광근[1]
명령형 언어가 튜링머신에 기반하고 있다면, 함수형 언어는 람다대수에 기반하고 있는 언어에 대한 총칭이다. 현업에서 많이 쓰이는 명령형 언어와는 대조적으로 몇 가지 특징이 있다.
2. 순수 함수형 언어는 변수가 없다.
"함수형 프로그래밍은 할당에 대해 규율을 부여한다."
Functional programming imposes discipline upon assignment.
로버트 C. 마틴, 클린 아키텍처
순수 함수형 언어에는 변수와 변수를 바꾸는 대입 연산자(C 언어를 예로 들면 Functional programming imposes discipline upon assignment.
로버트 C. 마틴, 클린 아키텍처
=
)가 없다. 명령형 언어에서 a=3
이 a에 3을 대입하라는 명령인 반면, 순수 함수형 언어에서는 a=3
을 수학에서 let a be 3.
같이 a를 3으로 '정의'하는 것으로 본다. 즉 한번 a를 뭐라고 정의했으면 그 정의는 유효 범위(scope) 내에서 값이 바뀌지 않는다! 그 유효 범위 안쪽에서 a를 다시 재정의 할 수도 있지만 내부 유효 범위에 한해서 'a'의 정의가 바뀌는거지 대입되는게 아니다. 안쪽과 바깥쪽이 서로 다른 의미의 'a'가 되는 것.이게 불러오는 가장 큰 차이가 명령의 '순서'가 의미 없다는 점이다. 명령형 언어에서는 맨 윗줄에
a=3
이 있더라도 저 아래에 등장하는 a가 여전히 3임을 보장할 수가 없다. 그렇기 때문에 a 값을 다른 값으로 업데이트 하기 '전'과 '후'의 결과 자체가 완전히 달라지고 순서가 매우 중요하지만, 순수 함수형 언어에서는 첫 줄에 a=3
이 있으면 유효 범위 전체에서 a는 그대로 3이다.[2] 그렇다면 굳이 a를 사용하기 '전'에 미리 정의할 필요가 없고, 유효 범위 내의 아무 곳에나 정의가 되어 있기만 하면 그걸 그냥 갖다 쓰는 방식으로도 아무런 문제가 없다.이런 특징에서 나오는 장점으로 표현식의 의미가 명료해진다는 것이 있다. 또, 제어 흐름을 생각하지 않고 프로그래밍 할 수 있다는 장점이 있다. 디버깅을 할 때도, 명령형 언어에서 버그를 잡을 때는 변수들의 전후 변화를 생각하면서 머리를 싸맬 때, 함수형 언어는 유효 범위만 잘 확인하면 쉽게 디버깅 할 수 있다. 절차형 언어와는 달리 눈에 핏발을 세우고 변수가 어떻게 변화하나 추적할 필요가 없다며 함수형 언어 팬들은 자랑하고는 한다.
단점으로는 값이 변경될 때마다 일일히 새 메모리에 저장해야하기 때문에 필연적으로 성능이 떨어진다. 프로그램이 돌아가면서 값을 갱신해야 하는 상황은 필연적으로, 그것도 상당히 자주 일어나기 마련인데 변수가 없어 값을 수정할 수가 없으니 새로운 메모리에 이를 저장해야 하며, 필요 없어진 메모리는 해제도 해줘야 한다. 쓰레기 수집 문서를 보면 알겠지만 이러한 메모리 사용은 성능을 엄청나게 떨어뜨린다. 이를 조금이나마 보완해보고자 지연 평가 개념이 있기는 하지만 제아무리 이를 잘 활용해도 명령형 언어의 성능을 따라잡는건 불가능하다.
3. destructive update
destructive update는 한 번 정의한 변수의 값을 차후에 다른 값으로 업데이트 하는 것이다.두 번째로 동반되는 명령형 언어와의 차이점이 destructive update인데, 예를 들어 일반적인 명령형 언어에서
a=a+1
는 a라는 변수에 a를 1만큼 증가시킨 값을 대입하라는 의미를 가지며 이 명령을 시행하는 시점에서 변수 a의 값이 바뀌게 된다.하지만 순수 함수형 언어인 하스켈의 경우
a=a+1
는 a라는 변수를 정의하는 것이 아니라 a라는 함수를 정의한다. 출력하게 하면 a=a+1=(a+1)+1=((a+1)+1)+1=(((a+1)+1)+1)+1=...
이런 식으로 무한 루프에 빠져서 영원히 a의 값을 출력하지 못한다. 이것은 lazy evaluation 때문이 아닌, 함수형 언어의 특징이다. 타 언어에 익숙하다면 저 a가 변수처럼 보일 수 있으나 함수를 정의하는 문법이다. f(x,y)
를 이변수함수로, f(x)
를 일변수함수로 보듯이, a 역시 f(void)
같은 파라미터가 0개인 함수이다. 함수형 언어에서의 a=3
은 그냥 C언어에서의 int a(void) { return 3; }
으로 보면 된다. 사실 이런 특징이 일반 프로그래머 기준으로 좀 괴악하기 때문에 대중적인 함수형 언어들 중에는 순수성을 포기하고 명령형 언어적인 부분을 포함하여 destructive update를 허용하는 경우도 있다. 이미 a의 다른 정의가 있었다면 컴파일러가 중복 정의가 있다며 에러를 뱉어낸다. 하스켈 같은 경우, 아예 a=a
를 ⊥(논리학에서의 falsum)로 정의한다.[3]일반 프로그래머의 상식으로는 도저히 이해가 안 될 결정인데 일부러 난해한 프로그래밍 언어라도 만들 생각으로 만들었을까? 그게 아니고 함수형 언어는 수학의 '함수'를 프로그래밍 언어 설계에 적극적으로 반영한 것이다. 수학의 함수는 정의상 입력이 같으면 출력도 같다. 그러니까
f(x)=x+1
인 함수를 정의했다면 f(1)=2
다. 다른 값은 절대 나오지 않는다. 함수형 언어의 함수도 마찬가지로 function foo(1)
의 수행 결과가 2였다면 언제 어느때든 foo(1)
은 2만 나온다. 하지만 함수형 언어가 아닌 언어에서는 foo(1)
이 3도 나올 수 있고 4도 나올 수 있다. 그러니까 C언어로 치면 이런 함수에 해당한다.#!syntax cpp
int inc(int a) {
static int c = 0;
c = c + a;
return c;
}
이 함수에 1을 넣어 여러 번 호출하면 1, 2, 3, 4, 5, ...가 나온다. 순수 함수형 언어는 이게 안 된다는 얘기다.이 특징으로 얻는 이득으로 함수형 언어는 메모이제이션[4]이 가능하다. 그 함수를 호출한 파라메터(
f(x)
에서 x)을 알고 있고 그것을 수행한 결과를 안다면 다음에 호출할 때는 그냥 메모해둔 결과값을 돌려주면 된다. 만약 피보나치 수열의 재귀함수 구현에 메모이제이션을 적용하면 극단적으로 속도가 빨라지는데 아예 가 으로 바뀌어 버리는 마법같은 일이 벌어진다.[5] 물론 절차형 프로그램도 외부 상태에 전혀 의존하지 않는 순수 함수를 구현하면 캐싱이 가능하긴 한데 그게 언어 차원에서 보장이 되느냐 프로그래머가 의도해야 하느냐의 차이가 있다.두 번째로 입력이 같으면 출력이 같다는 게 언어 차원에서 보장되기 때문에 손쉽게 병렬화가 가능하다. 최근에 함수형 언어가 다시 각광받는 이유 중에 하나로, 멀티코어 프로세싱이 요구되고 있는 현 상황에서 떠오르는 강력한 장점으로 꼽힌다. 멀티스레드 프로그래밍에서 버그 발생 원인은 스레드간 공유되는 메모리를 변경하는 것인데, 함수형 언어는 메모리가 불변이기 때문에 언어 차원에서 멀티스레드의 안정성이 보장되기 때문이다.
자료구조 상 destructive update가 허용되지 않기 때문에 효율적인 자료구조와 알고리즘도 명령형 언어에 비해 상당히 달라지게 된다. 일반적으로 저런 destructive update를 사용하는 자료구조를 ephemeral data structure라 하며, 순수 함수형 언어에서 사용되는 자료구조를 persistent data structure[6]라 한다.
4. 함수가 대단히 특별하지 않다.
그리고 함수를 first-class datatype[7]으로 분류하기에 함수를 그냥 보통 변수 다루듯 할 수 있다. 즉 함수를 다른 함수에 인수로 바로 넘겨줄 수도 있고, 함수를 만드는 함수(함수를 반환값으로 가지는 함수)를 정의할 수도 있으며 생산성이 매우 뛰어나다. 코드가 매우 간결해지며[8] 버그가 잘 생기지 않는 견고한 코드가 나오는 경향이 있다.5. 함수형 언어 예시
함수형 언어의 시초는 아주 옛날에 개발된 LISP부터 시작해서 그 방언 스킴(Scheme) 등이 있었고, 그 Scheme의 방언이며 자바 가상 머신에서 실행되는 클로저(Clojure), 전화교환기용 언어에서 출발한 Erlang, 타입 검증용 언어에서 시작된 ML #, 극단적인 언어 디자인으로 유명한 Haskell 등이 있다.- Clojure
- Erlang
- Haskell
- LISP
- Miranda
- ML
- Scheme
6. 전망
다만 함수형 프로그래밍이 완전한 메이저 패러다임이 될 수 있는지에 대해서는 회의적인 의견이 많다. 기본적으로 추상화 단계가 지나치게 높아져서 절대 다수의 개발자들에겐 코드 리딩이 어렵고, 기존의 자료구조를 상당히 들어엎어야 한다는 부담이 있다. 또한 함수형 프로그래밍을 추구하는 Clojure, Haskell 등은 대중성과는 한참 거리가 멀기 때문에 시장이 확대되기에도 무리가 있다.[9] 한 예로 C++만 해도 위에서 함수형 언어의 기능을 탑재하는 중이라고 했지만 여전히 업계에서는 잘 사용되지 않고 있으며, 함수형 언어의 장점인 병렬/분산 프로그래밍 또한 실제로는 기존 명령형 언어들의 기능을 가지고도 충분히 구현 가능한 경우가 대부분이다.7. 기타
- 참고로 객체 지향 언어와 함수형 언어는 서로 배타적인 개념이 아니라 얼마든지 섞어 쓸 수 있다. 최근 함수형 패러다임이 유명세를 타면서 C++이나 파이썬 등 명령형 언어들에서 앞다투어 함수형 언어의 기능을 탑재하고 있다. 또, F#, Scala, OCaml 같이 OOP와 함수형 프로그래밍을 짬뽕해놓은 멀티 패러다임 언어들도 많다. 사실 JavaScript도 함수형 패러다임을 포함한다. 생산자-소비자 패턴과 같이, 객체 지향 언어에서 함수형 언어의 특징을 살린 패턴도 있다.[10] 그러니까 굳이 함수형 언어를 안 배우더라도 함수형 패러다임과 알고리즘 정도는 배워두면 어느 정도 도움이 된다.
- liftIO라는 함수형 프로그래머를 위한 행사가 있다.
8. 외부 링크
- https://devm.io/programming/disadvantages-of-purely-functional-programming-126776
- https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf
[1] http://kwangkeunyi.snu.ac.kr/functional.html[2] 하위 유효 범위에서 덮어쓰기는 가능하다. 함수형 언어에서는 이것을 섀도(shadow)라 표현한다. 명령형 언어에서는 포인터 등을 이용하여 하위 유효 범위에서 아예 상위 유효 범위 변수 자체의 값을 바꿔 버리는 것도 가능하지만, 함수형 언어에서는 하위 유효 범위에서 섀도 된 값은 해당 유효 범위 바깥에 절대 영향이 없다.[3] 물론, 언어 내의 Boolean type의 false가 아닌, 컴퓨팅 모델 자체의 false 값을 의미한다.[4] Memoization. Memorization이 아닌 것에 주의![5] 이를 동적 계획법(Dynamic Programming)이라고 한다.[6] 자료구조를 업데이트 할때마다 계속 새로 자료구조를 만드는 모델. 순수 함수형 언어의 경우 한번 정의한 값을 바꿀 수가 없어서 이게 선택이 아닌 필연이 된다. fully persistent data structure에서는 심지어 이전 버전의 자료에도 접근할 수 있다.[7] 다른 데이터와 똑같은 취급을 받는다는 뜻. 대조적으로 자바의 메소드나 C의 함수를 생각해보자. 자바에서 인자로 메소드를 주거나 리턴받는 것은 불가능하고, C에서도 포인터가 아니라 함수 자체를 넘겨주는 건 불가능하다. 또한 이 개념은 언어 내장 데이터 타입(Primitive)과는 다르다. 파이썬 같이 분명히 함수형이 아닌 언어에서도 사용하는 경우는 있다.[8] 심지어 하스켈 같은 경우 너무 간결해서 오히려 이해하기가 어려운 것 같다는 불평이 나오기도 할 정도[9] 그나마 Scala가 Java의 보조 언어로 쓰이고 있는 정도다.[10] 메시지(생산물)를 통해 메시지를 전달하고, 객체 내부의 변수에 대한 직접 접근을 막는다.