함수형 프로그래밍
함수형 프로그래밍
함수형 프로그래밍(Functional Programming, FP)은 계산을 수학적 함수의 평가로 다루고, 상태 변경과 가변 데이터를 피하는 프로그래밍 패러다임이다. 한국어로는 보통 함수형 프로그래밍 또는 함수형 패러다임이라고 부른다.
함수형 프로그래밍은 객체지향 프로그래밍과 함께 현대 산업의 양대 패러다임이지만, 그 이론적 뿌리는 더 오래되었다. 객체지향이 1960년대 시뮬레이션과 GUI 작업에서 형성된 반면, 함수형 프로그래밍의 기반인 람다 계산법(lambda calculus)은 1930년대 수리논리학에서 출발했다. 따라서 함수형 프로그래밍은 "오래된 새 패러다임"이라고 불리기도 한다.
객체지향 = 데이터와 그 데이터를 조작하는 행위를 객체 단위로 묶는다
함수형 = 데이터를 변환하는 순수 함수의 합성으로 프로그램을 구성한다
핵심 정의
함수형 프로그래밍은 다음 성질을 강하게 강조하는 패러다임이다.
1. 함수가 일급 시민(first-class)이다.
2. 함수는 수학적 의미의 함수에 가깝다 (동일 입력 → 동일 출력).
3. 상태 변경(mutation)과 부수 효과(side effect)를 회피한다.
4. 반복(loop)보다 재귀(recursion)와 고차 함수(higher-order)를 선호한다.
5. 자료는 변경되지 않고 새 자료를 만든다 (immutability).
위 다섯 가지가 모두 100% 강제되는 언어는 드물다. 강제 정도가 다를 뿐, 함수형 사고방식은 거의 모든 현대 언어에서 부분적으로 채택되어 있다.
역사
함수형 프로그래밍의 역사는 산업이 아니라 수리논리학에서 시작된다. 이 출발점이 함수형 프로그래밍의 문화와 어휘를 객체지향과 다르게 만들었다.
람다 계산법 (1930년대)
Alonzo Church가 1932-1936년에 형식화한 람다 계산법은 함수 추상화와 적용만으로 모든 계산을 표현할 수 있음을 보였다. 처치-튜링 명제(Church-Turing thesis)는 람다 계산법과 튜링 기계가 동등한 계산력을 가짐을 주장한다.
람다 계산법의 기본 연산:
- 변수: x
- 추상: λx. M (x를 받아 M을 반환하는 함수)
- 적용: M N (함수 M에 인자 N을 적용)
이 세 가지 구문만으로 자연수, 불, 자료구조, 재귀, 모든 계산을 표현할 수 있다. 현대 함수형 언어의 모든 핵심 개념은 람다 계산법의 직접적 후손이다.
Lisp (1958)
John McCarthy가 1958년 발표한 Lisp(LISt Processor)는 람다 계산법에서 영감을 받은 최초의 프로그래밍 언어다. McCarthy의 1960년 논문 "Recursive Functions of Symbolic Expressions and Their Computation by Machine"이 기초 문헌이다.5(#ref-5)
다만 Lisp가 "함수형 언어"인지에 대해서는 주의가 필요하다. Lisp는 함수형 사고방식의 핵심 요소(일급 함수, 재귀 중심, 동질 자료/코드)를 도입했지만, 가변 변수와 부수 효과를 자유롭게 허용했다. 즉 Lisp는 함수형의 출발점이지 순수 함수형은 아니다. 이 점은 Lisp를 "함수형의 원조"라고 단정하는 흔한 진술의 정확도를 떨어뜨린다.
ISWIM과 이론적 기반 (1960년대)
Peter Landin은 1966년 논문 "The Next 700 Programming Languages"에서 가상 언어 ISWIM(If you See What I Mean)을 제시했다.6(#ref-6) ISWIM은 실제로 구현되지 않았지만, 이후 함수형 언어의 구문과 의미론에 큰 영향을 끼쳤다. let/where 구문, 표현식 중심 설계, 오프사이드 규칙 계열의 사고방식이 이 흐름과 연결된다.
Backus의 튜링 강연 (1977)
John Backus는 1977년 튜링 수상 강연 "Can Programming Be Liberated from the Von Neumann Style?"에서 명령형 프로그래밍이 폰 노이만 구조에 너무 묶여 있다고 비판하고, 함수 수준(function-level) 프로그래밍을 제안했다.1(#ref-1) 이 강연은 함수형 프로그래밍 운동의 산업적 정당화로 자주 인용된다.
흥미로운 사실: Backus는 FORTRAN의 설계자였다. 즉 명령형 프로그래밍의 대표 언어를 만든 사람이 그 패러다임의 한계를 지적했다는 점에서 이 강연은 더 무게를 가진다.
ML 계열 (1973)
Robin Milner가 LCF(Logic for Computable Functions) 정리 증명기를 위해 1973년 무렵 만든 ML(Meta Language)은 정적 타입 추론을 함수형 언어에 결합한 대표적 사례다.9(#ref-9) Hindley-Milner 타입 시스템은 ML 계열을 통해 널리 알려졌고, 이후 정적 타입 함수형 언어의 기반이 되었다.
ML 계열 후예:
Standard ML (1983)
Caml (1985) / OCaml (1996)
F# (2005) - Microsoft, OCaml 기반
영향 받음: Rust, Swift, Scala의 타입 시스템
Miranda와 게으른 평가 (1985)
David Turner가 1985년 발표한 Miranda는 게으른 평가(lazy evaluation), 순수 함수, 대수적 자료형을 결합한 상업 지원 순수 함수형 언어다.7(#ref-7) Miranda는 상표권 문제로 학계에서 공통 연구 언어로 쓰기 어려웠고, 여러 게으른 함수형 언어를 통합할 공개 표준의 필요성이 Haskell 탄생 배경 중 하나가 되었다.
Haskell (1990)
Haskell은 1987년 게으른 평가 기반 함수형 언어의 공통 표준을 만들기 위한 위원회로 시작해 1990년 첫 표준(Haskell 1.0)을 발표했다.8(#ref-8) 이름은 람다 계산법 연구자 Haskell Curry에서 왔다.
Haskell의 결정적 기여:
게으른 평가를 기본으로 채택
I/O를 모나드로 분리하여 순수성 유지
타입 클래스 도입 (Wadler & Blott, 1989)
학문과 산업 양쪽에서 함수형 사고방식의 표준 참조점이 됨
개인 메모: "하스켈로 프로그래밍하면 버그도 없지만 완성도 못 한다"는 농담이 있다. 나는 이 농담을 좋아한다.
동시성 중심 함수형 (Erlang, 1986)
Joe Armstrong을 비롯한 Ericsson 연구진이 1986년에 만든 Erlang은 통신 시스템의 동시성과 신뢰성을 위해 설계된 함수형 언어다. 액터 모델(actor model), 경량 프로세스, "let it crash" 철학으로 분산 시스템 분야의 사실상 표준 중 하나가 되었다. Elixir(2011)는 Erlang VM 위에 현대적 구문을 얹은 후속이다.
모던 함수형의 재부상 (2000년대 이후)
산업 영역에서 함수형 프로그래밍이 다시 주목받기 시작한 계기:
멀티코어 시대 도래 - 가변 상태의 동시성 비용이 커짐
Scala (2003), Clojure (2007), F# (2005) 등 JVM/CLR 함수형 언어 등장
Rust (2010-) - 함수형 영향이 강한 시스템 언어
React + Redux (2015-) - JS 프론트엔드에 단방향 데이터 흐름과 불변성 도입
TypeScript (2012-) - discriminated union, type-level 표현력
함수형 프로그래밍은 이제 "특수한 패러다임"이 아니라 모든 현대 언어에 부분적으로 흡수된 상태다.
핵심 개념
일급 함수 (First-class Function)
함수가 다른 값과 동등하게 다뤄진다는 의미.
- 변수에 함수를 할당할 수 있다
- 함수를 다른 함수의 인자로 넘길 수 있다
- 함수를 다른 함수의 반환값으로 받을 수 있다
- 자료구조에 함수를 저장할 수 있다
일급 함수가 가능해야 고차 함수가 의미를 가진다.
순수 함수 (Pure Function)
다음 두 조건을 모두 만족하는 함수:
1. 동일한 입력에 대해 항상 동일한 출력을 반환한다.
2. 외부 상태에 부수 효과(side effect)를 일으키지 않는다.
부수 효과의 예: 전역 변수 변경, 콘솔 출력, 파일 쓰기, 네트워크 통신, 예외 발생.
순수 함수의 가치는 참조 투명성(referential transparency)에서 온다. 표현식을 그 결과 값으로 치환해도 프로그램 의미가 바뀌지 않는다는 성질이며, 이게 함수형 추론과 최적화의 기반이다.
불변성 (Immutability)
값이 한 번 만들어지면 변경되지 않는다는 성질. 변경이 필요하면 새 값을 만든다.
-- Haskell: 리스트에 원소 추가는 새 리스트를 만든다
xs = [1, 2, 3]
ys = 0 : xs -- ys = [0, 1, 2, 3], xs는 그대로
불변성의 비용은 메모리 할당이지만, 영속 자료구조(persistent data structure)와 구조 공유(structural sharing)로 실용 수준까지 최적화된다.
고차 함수 (Higher-order Function)
함수를 인자로 받거나 함수를 반환하는 함수. 함수형 프로그래밍의 추상화 단위.
-- map, filter, fold가 대표
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
foldr :: (a -> b -> b) -> b -> [a] -> b
객체지향에서 다형성과 상속이 추상화 도구라면, 함수형에서는 고차 함수가 그 역할을 한다.
함수 합성 (Function Composition)
두 함수를 이어 하나의 함수로 만드는 연산.
-- Haskell: . 연산자
double :: Int -> Int
double x = x * 2
negate :: Int -> Int
negate x = -x
doubleNegate :: Int -> Int
doubleNegate = negate . double -- 먼저 double, 다음 negate
함수 합성은 함수형의 모듈성 단위다. 객체지향이 객체를 조립한다면 함수형은 함수를 합성한다.
커링과 부분 적용 (Currying / Partial Application)
여러 인자를 받는 함수를 한 인자씩 받는 함수의 연속으로 표현하는 변환. Haskell Curry의 이름에서 유래.
-- Haskell에서는 모든 함수가 자동으로 커링된 형태다
add :: Int -> Int -> Int
add x y = x + y
addOne :: Int -> Int
addOne = add 1 -- 부분 적용
커링이 자동인 언어(Haskell, OCaml)와 수동인 언어(JavaScript, Python)가 있다.
게으른 평가 (Lazy Evaluation)
값이 실제로 필요한 시점까지 계산을 미루는 평가 전략. Haskell이 대표적.
-- 무한 리스트도 정의 가능
naturals = [1..]
take 5 naturals -- [1, 2, 3, 4, 5]
게으른 평가의 장점은 합성 가능성과 추상화이지만, 단점도 명확하다: 공간 누수(space leak)와 디버깅 어려움. 게으른 평가가 무조건 좋은 게 아니라는 점은 함수형 안티패턴 섹션에서 다룬다.
재귀와 꼬리 호출 최적화
함수형 언어에서는 반복문 대신 재귀를 쓴다. 단순 재귀는 스택을 소비하므로, 꼬리 호출 위치의 재귀를 컴파일러가 반복문으로 변환하는 꼬리 호출 최적화(TCO, Tail Call Optimization)가 중요하다.
-- 꼬리 재귀가 아닌 합계 (스택 누적)
sumBad :: [Int] -> Int
sumBad [] = 0
sumBad (x:xs) = x + sumBad xs
-- 꼬리 재귀 합계 (누산기 사용)
sumGood :: Int -> [Int] -> Int
sumGood acc [] = acc
sumGood acc (x:xs) = sumGood (acc + x) xs
Scheme과 Erlang처럼 꼬리 호출을 언어/런타임 설계의 핵심으로 둔 언어는 이 최적화가 안정적이다. OCaml도 꼬리 재귀를 실용적으로 강하게 지원한다. 다만 Haskell은 게으른 평가 때문에 "꼬리 재귀면 무조건 스택/힙이 안전하다"라고 말하면 틀린다. 누산기가 지연된 thunk로 쌓이면 스택보다 힙이 터지는 공간 누수가 생길 수 있고, 이때는 seq, bang pattern, foldl' 같은 엄격성 제어가 필요하다.10(#ref-10)
타입 시스템과 결합한 함수형 개념
대수적 자료형 (Algebraic Data Type)
곱 타입(product, AND)과 합 타입(sum, OR)의 조합으로 자료를 표현한다.
-- 곱 타입 (struct/record와 유사)
data Point = Point Double Double
-- 합 타입 (tagged union)
data Shape = Circle Point Double
| Rectangle Point Point
| Triangle Point Point Point
Result 타입, Option 타입, 트리, 리스트, 모두 대수적 자료형으로 표현된다.
패턴 매칭 (Pattern Matching)
대수적 자료형을 분해하면서 분기하는 구문.
area :: Shape -> Double
area (Circle _ r) = pi * r * r
area (Rectangle (Point x1 y1) (Point x2 y2)) =
abs (x2 - x1) * abs (y2 - y1)
area (Triangle ...) = ...
컴파일러가 모든 경우를 다뤘는지 검사하는 exhaustiveness check가 핵심 가치다. 객체지향의 다형성이 런타임 디스패치라면 함수형의 패턴 매칭은 컴파일 타임 디스패치에 가깝다.
타입 클래스 (Type Class)
Haskell이 도입한 ad-hoc 다형성 메커니즘. Philip Wadler와 Stephen Blott가 1989년 논문 "How to Make Ad-Hoc Polymorphism Less Ad Hoc"에서 제안했다.2(#ref-2)
class Eq a where
(==) :: a -> a -> Bool
instance Eq Int where
x == y = primIntEq x y
Rust의 trait, Scala의 implicit/given, Swift의 protocol은 모두 "타입에 대한 연산 집합을 추상화한다"는 같은 문제 공간에 있다. 특히 Rust trait과 Scala type class 패턴은 Haskell 타입 클래스와 자주 비교된다. 다만 각 언어의 해법은 모듈 시스템, 객체 시스템, 컴파일러 제약이 달라 완전히 같은 것은 아니다.
매개변수 다형성 (Parametric Polymorphism)
타입을 매개변수로 받는 함수. C++ 템플릿, Java 제네릭과 유사하지만 함수형 언어에서는 더 깊게 통합된다.
identity :: a -> a
identity x = x
length :: [a] -> Int
타입 변수 a는 어떤 타입으로도 인스턴스화될 수 있다.
부수 효과 다루기
함수형 프로그래밍의 가장 까다로운 영역. 순수 함수만으로는 I/O, 상태, 예외를 표현할 수 없다.
모나드 (Monad)
부수 효과를 타입에 표현하여 순수성을 유지하는 추상. Eugenio Moggi가 1991년 논문 "Notions of Computation and Monads"에서 계산의 의미론으로 도입했고, Philip Wadler가 1990년대에 Haskell에서 실용화했다.3(#ref-3)
-- IO 모나드: 부수 효과가 타입에 명시된다
main :: IO ()
main = do
name <- getLine -- IO String
putStrLn ("Hello, " ++ name)
getLine이 IO String을 반환한다는 것은 "문자열을 만드는 IO 작업"이라는 의미이지 "문자열 그 자체"가 아니다. 이 타입 차이가 순수 함수와 부수 효과를 분리한다.
흔한 오해 정정:
모나드 = 카테고리 이론에서 빌려온 형식이지만, 사용 자체는 카테고리 이론 지식이 필요하지 않다
모나드는 if/then/else처럼 프로그래밍 도구일 뿐 신비한 개념이 아니다
개인 메모: 모나드를 처음 볼 때 가장 위험한 순간은 "아, 이제 알았다"라고 말하는 순간이다. 보통 그때부터 다시 모른다.
효과 시스템 (Effect System)
모나드 대신 부수 효과 자체를 타입으로 표현하는 더 일반적인 접근. 대수적 효과(algebraic effects)가 최근 연구 영역. Eff, Koka, Unison 같은 언어가 시도하고 있다.
모나드: 한 가지 효과를 하나의 타입으로 (IO, State, Reader 등)
효과 시스템: 여러 효과를 자유롭게 조합 가능
함수형 프로그래밍 vs 객체지향 프로그래밍
두 패러다임은 같은 문제를 다른 축으로 분해한다.
객체지향 함수형
상태 객체 내부에 캡슐화 불변, 함수 외부로 명시
다형성 서브타이핑 + 다형성 매개변수 + 타입 클래스
재사용 상속, 합성, 위임 함수 합성, 고차 함수
시간 객체의 상태 변화 값의 변환 흐름
오류 예외 + try/catch Result/Either, 모나드
모듈 클래스, 패키지 모듈, 함수, 타입
같은 문제, 다른 분해:
"주문 상태를 추적하라"
객체지향: Order 객체가 status 필드를 가지고 메서드로 상태를 바꾼다
함수형: Order는 불변. 상태 전이는 새 Order를 만드는 함수 (Order -> Order)
두 접근은 양립 가능하다. 현대 언어 대부분은 두 패러다임을 함께 지원한다(Scala, Rust, C#, TypeScript). 어느 쪽을 기본 사고방식으로 쓸 것인가의 차이일 뿐 양자택일은 아니다.
순수성 스펙트럼
모든 함수형 언어가 같은 수준의 순수성을 강제하지 않는다.
[순수성 강함]
Haskell, Idris, Agda, Elm
- 부수 효과를 타입으로 분리
- Haskell은 게으른 평가가 기본
- Elm, Idris, Agda는 순수성/타입 discipline이 강하지만 평가 전략은 Haskell과 다르다
ML 계열 (Standard ML, OCaml, F#)
- 가변 참조 허용하지만 명시적
- 엄격한 평가가 기본
Lisp 계열 (Clojure, Common Lisp, Scheme, Racket)
- Clojure는 불변 자료구조 기본
- 다른 Lisp는 자유로운 가변성
- 동적 타입
JVM/네이티브 다중 패러다임 (Scala, Rust, Swift)
- 함수형 도구를 강하게 지원
- OOP와 함께 사용
함수형 영향이 있는 명령형 (C#, JavaScript/TypeScript, Python, Java)
- 람다, 고차 함수, 컴프리헨션 등 흡수
- 핵심은 여전히 OOP/명령형
[순수성 약함]
특정 언어가 "함수형이냐 아니냐"는 이분법이 아니라 스펙트럼 어디에 위치하는지의 문제다.
대표 기법
map / filter / fold
리스트 변환의 3대 기본 연산.
map (*2) [1,2,3,4] -- [2,4,6,8]
filter (>2) [1,2,3,4] -- [3,4]
foldr (+) 0 [1,2,3,4] -- 10
명령형으로는 for 루프 + 누산기 + 조건문이 필요한 작업이 한 줄 표현으로 압축된다.
함수 합성과 파이프라인
-- Haskell: 오른쪽에서 왼쪽
processData :: [RawData] -> [Result]
processData = map normalize . filter isValid . map parse
// F#: 왼쪽에서 오른쪽 (|> 파이프 연산자)
let processData rawData =
rawData
|> List.map parse
|> List.filter isValid
|> List.map normalize
Point-free 스타일
인자를 명시하지 않고 함수 합성만으로 정의.
-- 일반 스타일
sumOfSquares xs = sum (map (^2) xs)
-- point-free
sumOfSquares = sum . map (^2)
가독성이 좋아지는 영역에서만 써야 한다. 과도하면 안티패턴.
영속 자료구조 (Persistent Data Structure)
불변성을 유지하면서도 부분 변경 시 전체 복사를 피하는 자료구조. 구조 공유로 O(log n) 또는 amortized O(1)의 수정이 가능하다.
대표 사례:
- Hash Array Mapped Trie (HAMT)
- Finger Tree
- Persistent Red-Black Tree
- Clojure의 PersistentVector / PersistentHashMap
- Haskell의 Data.Map (균형 이진 트리)
Chris Okasaki의 1998년 책 "Purely Functional Data Structures"가 이 분야의 표준 참고서다.4(#ref-4)
불변성은 실제로 어떻게 구현되는가
불변성은 "메모리를 안 쓴다"는 뜻이 아니다. 오히려 많은 함수형 구현은 힙에 새 노드를 만들고, 기존 노드를 공유한다. 핵심은 기존 값을 제자리에서 덮어쓰지 않는다는 점이다.
명령형 업데이트:
배열/객체의 같은 위치를 덮어쓴다.
함수형 업데이트:
바뀐 경로의 노드만 새로 만들고,
나머지 하위 구조는 기존 노드를 공유한다.
리스트의 구조 공유
Haskell식 리스트는 개념적으로 cons cell의 연결이다.
xs = [1, 2, 3]
ys = 0 : xs
메모리 관점에서는 대략 이렇게 볼 수 있다.
xs -> [1] -> [2] -> [3] -> []
ys -> [0] -> [1] -> [2] -> [3] -> []
^
xs와 tail 공유
ys를 만들 때 [1, 2, 3] 전체를 복사하지 않는다. 새 cons cell [0]만 만들고, tail은 xs가 가리키던 기존 리스트를 공유한다. 그래서 리스트 앞에 원소를 붙이는 (:)는 싸다. 반대로 리스트 뒤에 붙이는 작업은 보통 기존 앞부분을 새로 만들어야 하므로 비싸다.
트리와 맵의 구조 공유
균형 트리 기반 map/set도 비슷하다. 어떤 키 하나를 바꾸면 루트에서 해당 키까지의 경로만 새로 만들고, 건드리지 않은 서브트리는 공유한다.
기존 트리:
A
/ \
B C
/ \
D E
E만 바꾼 새 트리:
A'
/ \
B' C <- 공유
/ \
D E' <- D도 공유
이게 영속 자료구조의 핵심이다. 이전 버전도 살아 있고, 새 버전도 살아 있다. "복사"가 아니라 "경로 복사 + 구조 공유"다.
스택과 힙 관점
함수 호출 자체는 여전히 스택 프레임을 만든다. 지역 변수, 반환 주소, 임시 계산 상태는 스택에 놓일 수 있다. 하지만 리스트 노드, 트리 노드, 클로저, 지연 계산 thunk처럼 수명이 호출 범위를 넘어갈 수 있는 값은 보통 힙에 놓인다.
스택:
- 함수 호출 프레임
- 짧은 수명의 지역 바인딩
- 평가 중인 임시 상태
힙:
- 리스트/트리/레코드 같은 자료구조 노드
- 클로저가 캡처한 환경
- Haskell의 지연 계산 thunk
- 공유되는 persistent data structure 노드
즉 함수형의 불변성은 "스택만 써서 빠르다"가 아니다. 오히려 힙 할당과 GC를 적극 활용하되, 구조 공유와 컴파일러 최적화로 비용을 줄인다.
개인 메모: 불변성은 공짜 점심이 아니다. 다만 계산서를 "복사 전체"가 아니라 "바뀐 경로"에만 청구하는 기술이라고 한다. 나는 외상만 한가득이다.
Haskell의 thunk와 공간 누수
Haskell은 게으른 평가 때문에 값을 바로 계산하지 않고 thunk를 힙에 저장할 수 있다.
sumBad = foldl (+) 0 [1..1000000]
위 코드는 직관적으로는 누산기를 계속 더하는 것처럼 보이지만, 게으른 평가에서는 다음과 같은 계산 덩어리가 쌓일 수 있다.
((((0 + 1) + 2) + 3) + ...)
이 덩어리들이 힙에 쌓이면 스택 오버플로우가 아니라 힙 사용량 폭증으로 나타난다. 그래서 Haskell 성능 문제는 종종 "스택이냐 힙이냐"보다 "어떤 thunk가 언제 강제로 평가되는가"가 핵심이다.
import Data.List (foldl')
sumGood = foldl' (+) 0 [1..1000000]
foldl'은 누산기를 매 단계 엄격하게 평가해서 thunk 누적을 줄인다.
실무 언어에서의 불변성
JavaScript/TypeScript, C#, Java 같은 주류 언어에서 "불변 업데이트"는 보통 런타임이 자동으로 영속 자료구조를 제공한다는 뜻이 아니다. 대개는 객체/배열을 얕게 복사하거나, Immutable.js, Immer, Clojure식 persistent collection 같은 라이브러리를 쓴다.
// 얕은 복사 기반 불변 업데이트
const next = {
...prev,
status: "paid",
};
이 코드는 prev를 바꾸지 않는다는 점에서는 불변 업데이트지만, 깊은 구조 전체가 자동으로 영속 자료구조가 되는 것은 아니다. 중첩 객체를 잘못 공유하면 여전히 내부 mutation 문제가 생긴다.
불변성 구현 방식:
- Haskell/Clojure: 언어/표준 자료구조 차원에서 구조 공유가 기본
- Scala/F#/OCaml: 불변 컬렉션과 가변 컬렉션을 선택
- JS/TS/React: 관례와 라이브러리로 불변 업데이트를 유지
- Rust: 소유권/빌림 규칙으로 aliasing과 mutation을 통제
언어별 함수형 예시
Haskell
-- 대수적 자료형 + 패턴 매칭 + 재귀
data Tree a = Leaf | Node a (Tree a) (Tree a)
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node x Leaf Leaf
insert x (Node y left right)
| x < y = Node y (insert x left) right
| x > y = Node y left (insert x right)
| otherwise = Node y left right
OCaml
(* 옵션 타입과 패턴 매칭 *)
let rec find_first pred = function
| [] -> None
| x :: rest when pred x -> Some x
| _ :: rest -> find_first pred rest
F#
// 파이프라인 중심
let processOrders orders =
orders
|> List.filter (fun o -> o.Status = Active)
|> List.map (fun o -> { o with Total = computeTotal o })
|> List.sortByDescending (fun o -> o.Total)
Clojure
;; 데이터 변환 파이프라인
(->> orders
(filter active?)
(map compute-total)
(sort-by :total >))
Scala
// for-comprehension (모나드 추상)
val result = for {
user <- findUser(id)
account <- findAccount(user)
balance <- getBalance(account)
} yield balance
JavaScript / TypeScript
// 고차 함수와 합성
const processOrders = (orders: Order[]): Result[] =>
orders
.filter(o => o.status === "active")
.map(o => ({ ...o, total: computeTotal(o) }))
.sort((a, b) => b.total - a.total);
C#
// LINQ는 사실상 함수형 쿼리 언어
var result = orders
.Where(o => o.Status == OrderStatus.Active)
.Select(o => o with { Total = ComputeTotal(o) })
.OrderByDescending(o => o.Total)
.ToList();
Rust
// Iterator 체이닝 + 클로저
let result: Vec<_> = orders
.iter()
.filter(|o| o.status == Status::Active)
.map(|o| Order { total: compute_total(o), ..o.clone() })
.collect();
동시성과 분산
함수형 프로그래밍이 멀티코어 시대에 부활한 주된 이유는 불변성이 동시성 비용을 낮추기 때문이다.
가변 상태의 동시성 비용:
- 락(lock) 관리
- 경쟁 조건(race condition)
- 데드락
- 메모리 일관성 모델
불변 상태의 동시성 이득:
- 락이 필요 없다 (읽기 전용)
- 부수 효과가 명시되어 추론 가능
- 액터 모델, 메시지 패싱과 자연스럽게 결합
대표 사례:
Erlang / Elixir: 액터 모델, 경량 프로세스, 고가용성 통신 시스템 사례로 유명
Akka (Scala/Java): 액터 기반 분산 시스템 프레임워크
Hadoop / Spark: MapReduce는 본질적으로 함수형 패러다임
React + Redux: 단방향 데이터 흐름, 상태는 불변, 변경은 reducer 함수
Elm: 프론트엔드에서 순수 함수형 강제
흔한 오해와 정정
오해 1: "함수형 프로그래밍에는 가변성이 전혀 없다"
부정확. 순수 함수형 언어조차 부수 효과를 다루는 메커니즘을 갖는다(Haskell의 IO 모나드, ST 모나드). 다중 패러다임 함수형 언어(OCaml, F#, Scala)는 가변 참조를 명시적으로 허용한다. 함수형의 핵심은 "변경 금지"가 아니라 "기본은 불변, 변경은 명시적"이다.
오해 2: "함수형은 객체를 안 쓴다"
부정확. F#, OCaml, Scala는 모두 객체 시스템을 가진다. Haskell도 레코드와 타입 클래스로 객체와 유사한 추상을 만든다. 함수형은 OOP의 부정이 아니라 다른 우선순위의 패러다임이다.
오해 3: "재귀가 항상 반복보다 낫다"
부정확. 꼬리 호출 최적화가 보장되지 않는 언어(JavaScript, Python)에서는 깊은 재귀가 스택 오버플로우를 일으킨다. 명확함이 같다면 언어가 자연스럽게 지원하는 형태를 따르는 게 옳다.
오해 4: "모나드는 카테고리 이론을 알아야 이해할 수 있다"
부정확. 모나드는 일관된 인터페이스를 가진 박스라는 직관으로 충분하다. 카테고리 이론은 모나드의 수학적 배경이지 사용 전제가 아니다.
오해 5: "함수형 프로그래밍은 산업에서 실용적이지 않다"
부정확. WhatsApp의 Erlang/FreeBSD 인프라, Jane Street의 OCaml, Meta/Facebook의 Haskell 기반 Sigma, Twitter의 Scala, Klarna의 Erlang 사례처럼 함수형 언어와 함수형 스타일은 실제 산업 시스템에서도 쓰인다.11(#ref-11)12(#ref-12)13(#ref-13)14(#ref-14)15(#ref-15) 산업 사용이 전혀 없는 게 아니라, 고가용성 통신, 금융, 규칙 엔진, 분산 서비스처럼 특정 도메인에 집중되는 경향이 있다는 게 더 정확하다.
개인 메모: 불변성은 공짜 점심이 아니다. 다만 계산서를 "복사 전체"가 아니라 "바뀐 경로"에만 청구하는 기술이라고 한다. 나는 외상만 한가득이다.
함수형 안티패턴
1. map 안에서 부수 효과
// 잘못된 사용: map은 결과 리스트를 만드는 게 목적
items.map(item => sendEmail(item)); // 결과 리스트가 버려진다
// 옳은 사용: 부수 효과는 forEach
items.forEach(item => sendEmail(item));
2. 과도한 point-free
-- 무엇을 하는지 알 수 없는 코드
processAll = ((.) . (.)) (flip (.)) (uncurry . curry)
point-free는 가독성이 좋아질 때만 써야 한다.
개인 메모: 이 정도 point-free 코드는 동료에게 지식을 전달하는 게 아니라 암호문을 제출하는 쪽에 가깝다.
3. 조기 모나드 추상화
-- 단순한 작업에 모나드 트랜스포머 스택을 쌓음
type App a = ReaderT Config (StateT World (ExceptT Error IO)) a
세 단계 stacked 모나드는 거의 항상 과도하다. 처음에는 단일 IO 또는 단일 Either로 시작해서 필요할 때 추상화를 올리는 게 옳다.
4. 게으른 평가로 인한 공간 누수
Haskell의 고전적 함정.
-- foldl은 게으름 때문에 큰 누적 thunk를 만든다
sumBad = foldl (+) 0 [1..1000000]
-- foldl'은 누산기를 즉시 평가한다
sumGood = foldl' (+) 0 [1..1000000]
foldl vs foldl'을 모르면 큰 리스트에서 메모리가 폭증한다. 이게 게으른 평가의 함정.
5. type class 계층을 OOP 클래스 계층처럼 사용
-- 안티패턴: 깊은 상속 계층을 type class로 흉내
class Animal a where ...
class Animal a => Mammal a where ...
class Mammal a => Dog a where ...
타입 클래스는 OOP 상속 계층의 대체재가 아니다. 다른 도구.
6. 중간 리스트 누적
-- 매 단계마다 중간 리스트가 만들어진다
result = sum . map (*2) . filter even . map (+1) $ list
리스트 융합(list fusion)이 자동으로 최적화하는 경우가 많지만, 명시적으로 스트림이나 폴드로 통합하는 게 안전.
함수형 프로그래밍의 비판
학습 곡선
수학적 어휘(monad, functor, applicative)와 추상적 사고가 진입 장벽을 높인다. 명령형/객체지향에 익숙한 개발자에게는 "다시 배우는" 과정이 필요하다.
I/O 모델의 복잡성
모나드 변환자(monad transformer)를 통한 효과 조합은 강력하지만 학습 비용이 크다. 효과 시스템(algebraic effects) 연구는 이를 단순화하려는 시도다.
메모리 할당 비용
불변성은 새 객체 할당을 빈번하게 발생시킨다. 영속 자료구조와 컴파일러 최적화로 완화되지만, 최저 지연(latency-critical) 시스템에서는 여전히 부담이 된다.
가독성 논쟁
point-free, 모나드 do-notation, 타입 클래스 등은 익숙해지면 강력하지만 초심자에게는 암호처럼 보인다. 코드 리뷰 비용이 높을 수 있다는 실용적 비판이 있다.
왜 지금 중요한가
함수형 프로그래밍이 2010년대 이후 다시 주목받는 이유는 단순한 유행이 아니다.
- 멀티코어 시대: 가변 상태 동시성 비용이 커짐
- 분산 시스템: 불변성과 메시지 패싱이 자연스러움
- 데이터 파이프라인: map/reduce는 본질적으로 함수형
- 프론트엔드 상태 관리: React/Redux의 단방향 흐름
- 타입 시스템 발전: TypeScript, Rust가 ML 계열 영향을 흡수
오늘날 "함수형 프로그래머가 된다"는 것은 Haskell 전업이 아니라 어느 언어를 쓰든 함수형 사고방식을 활용할 줄 안다는 의미에 더 가깝다.
학습 경로
함수형 사고방식 입문은 언어 선택보다 사고 전환이 더 중요하다.
1. 익숙한 언어에서 시작
- JavaScript의 map/filter/reduce
- Python의 list comprehension, functools
- C#의 LINQ
- 가변성을 줄이고 함수 합성을 의식적으로 사용
2. 다중 패러다임 함수형 언어로 확장
- F#, OCaml, Scala 중 하나
- 타입 시스템과 패턴 매칭에 익숙해지기
3. 순수 함수형 도전
- Haskell 또는 Elm
- 모나드, 타입 클래스, 게으른 평가 학습
4. 이론적 기반 보강
- 람다 계산법
- 타입 이론 입문 (Pierce "Types and Programming Languages")
- 카테고리 이론 (선택사항)
핵심 질문:
이 문제를 데이터 변환의 흐름으로 표현할 수 있는가?
이 상태 변경을 새 값 생성으로 바꿀 수 있는가?
이 부수 효과를 타입으로 명시할 수 있는가?
이 반복을 고차 함수로 표현할 수 있는가?
요약
함수형 프로그래밍은 1930년대 람다 계산법에 뿌리를 두고, Lisp(1958), ML(1973), Haskell(1990)을 거쳐 현대로 이어진 패러다임이다. 핵심은 다음 다섯 가지다.
1. 함수는 일급 시민이다
2. 함수는 수학적 함수에 가깝다 (순수성)
3. 자료는 불변이다
4. 부수 효과는 명시된다
5. 추상화 단위는 고차 함수와 함수 합성이다
함수형 프로그래밍은 객체지향의 대체재가 아니라 짝이다. 멀티코어와 분산 시스템 시대에 다시 주목받고 있고, 현대 언어 대부분에 부분적으로 흡수되어 있다. "함수형 프로그래머"는 특정 언어 사용자가 아니라 함수형 사고방식을 활용할 줄 아는 개발자에 가깝다.
같이 보기
프로그래밍 패러다임
객체지향 프로그래밍
디자인 패턴
프로그래밍 이디엄
람다 계산법
모나드
타입 시스템
대수적 자료형
Result 타입
Option 타입
영속 자료구조
참조 투명성
고차 함수
함수 합성
게으른 평가
꼬리 호출 최적화
Hindley-Milner 타입 시스템
액터 모델
참고문헌
[1] Backus, John (1978). "Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs". Communications of the ACM. 21 (8): 613–641. doi:10.1145/359576.359579.
[2] Wadler, Philip; Blott, Stephen (1989). "How to Make Ad-Hoc Polymorphism Less Ad Hoc". POPL '89: Proceedings of the 16th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages. pp. 60–76. doi:10.1145/75277.75283.
[3] Wadler, Philip (1995). "Monads for functional programming". Advanced Functional Programming. Lecture Notes in Computer Science. Vol. 925. pp. 24–52. doi:10.1007/3-540-59451-5_2. Moggi의 이론적 출발점은 "Notions of Computation and Monads"도 함께 봐야 한다.
[4] Okasaki, Chris (1998). Purely Functional Data Structures. Cambridge University Press. ISBN 0-521-66350-4.
[5] McCarthy, John (1960). "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I". Communications of the ACM. 3 (4): 184–195.
[6] Landin, Peter J. (1966). "The Next 700 Programming Languages". Communications of the ACM. 9 (3): 157–166.
[7] Turner, David. The Miranda Programming Language. Miranda는 Research Software Ltd.가 상업 지원한 lazy/pure functional language로 Haskell에 직접 영향을 주었다.
[8] Hudak, Paul; Hughes, John; Peyton Jones, Simon; Wadler, Philip (2007). "A History of Haskell: Being Lazy With Class". HOPL III.
[9] Milner, Robin; Tofte, Mads; Harper, Robert; MacQueen, David (1997). The Definition of Standard ML (Revised). MIT Press. ML의 역사적 배경은 LCF 정리 증명기와 연결된다.
[10] GHC User's Guide. "Bang patterns and Strict Haskell". GHC는 lazy evaluation이 기본이고, 성능이 중요한 코드에서는 thunk 제거와 strictness 제어가 중요하다고 설명한다.
[11] Reed, Rick. "That's 'Billion' with a 'B': Scaling to the Next Level at WhatsApp". InfoQ / Erlang Factory SF Bay, 2014. WhatsApp의 Erlang/FreeBSD 기반 확장 사례.
[12] Jane Street. "Technology at Jane Street". Jane Street는 가능한 많은 시스템을 OCaml로 작성한다고 설명한다.
[13] Meta Engineering. "Fighting spam with Haskell". Facebook/Meta의 Sigma rule engine Haskell 적용 사례.
[14] Twitter. "Effective Scala". Twitter의 Scala 사용과 함수형/표현식 중심 스타일에 대한 내부 가이드.
[15] Erlang Solutions. "Klarna case study". Klarna의 Erlang 기반 고가용성 결제 시스템 사례.
추가 참고:
Hughes, John (1989). "Why Functional Programming Matters". The Computer Journal. 32 (2): 98–107.
Hudak, Paul (1989). "Conception, Evolution, and Application of Functional Programming Languages". ACM Computing Surveys. 21 (3): 359–411.
Moggi, Eugenio (1991). "Notions of computation and monads". Information and Computation. 93 (1): 55–92.
Pierce, Benjamin C. (2002). Types and Programming Languages. MIT Press.
외부 링크
분류: 프로그래밍 | 프로그래밍 패러다임 | 프로그래밍 언어 | 소프트웨어 공학 | WIKI