오늘은 함수형 사고를 보는 대표적인 면접 문제를 정리해보겠습니다. 처음 보면 낯설지만, 핵심 원리만 이해하면 클로저와 함수를 값처럼 다루는 방식이 왜 중요한지 한 번에 감이 옵니다.
문제
cons(a, b)는 두 값을 하나의 쌍처럼 묶어 주는 함수입니다. 그리고 다음 두 함수가 있다고 가정합니다.
car(pair) — 쌍의 첫 번째 값을 반환
cdr(pair) — 쌍의 두 번째 값을 반환
예를 들면 다음과 같습니다.
car(cons(3, 4)) # 3
cdr(cons(3, 4)) # 4
다음과 같이 cons가 구현되어 있을 때, car와 cdr를 구현하는 것이 문제입니다.
def cons(a, b):
def pair(f):
return f(a, b)
return pair
처음 보면 왜 헷갈릴까?
보통 pair라고 하면 (a, b) 같은 자료구조를 떠올립니다. 그런데 여기서는 값을 직접 저장해서 꺼내는 방식이 아닙니다.
핵심
cons(a, b)는 실제로 pair 데이터를 반환하는 것이 아니라, 나중에 어떤 함수를 받으면 그 함수에 (a, b)를 넣어 실행해주는 함수를 반환합니다.
즉, cons(3, 4)의 결과는 이런 느낌입니다.
"함수 하나를 주면, 내가 그 함수에 3과 4를 넣어서 실행해줄게"
풀이 아이디어
car는 pair에게 첫 번째 값만 반환하는 함수를 넘기면 됩니다.
def first(a, b):
return a
cdr는 pair에게 두 번째 값만 반환하는 함수를 넘기면 됩니다.
def second(a, b):
return b
정답 코드
def cons(a, b):
def pair(f):
return f(a, b)
return pair
def car(pair):
def first(a, b):
return a
return pair(first)
def cdr(pair):
def second(a, b):
return b
return pair(second)
더 짧게 쓰는 방법
def cons(a, b):
return lambda f: f(a, b)
def car(pair):
return pair(lambda a, b: a)
def cdr(pair):
return pair(lambda a, b: b)
실행 예시
p = cons(3, 4)
print(car(p)) # 3
print(cdr(p)) # 4
왜 이런 방식이 가능한가?
1. 클로저
cons(a, b)가 끝난 뒤에도 내부 함수 pair는 바깥 변수였던 a와 b를 기억합니다.
2. 함수도 값처럼 다룰 수 있다
파이썬에서는 함수를 다른 함수의 인자로 넘길 수 있습니다. 이 문제는 그 성질을 아주 직접적으로 보여줍니다.
3. 데이터를 함수로 표현할 수 있다
pair를 리스트나 튜플 같은 자료구조로 저장하지 않고, 함수 자체로 표현했기 때문입니다.
일반적인 pair는 서랍 안에 값 2개를 넣어두는 방식입니다. 반면 이 문제의 pair는 값 2개를 기억하고 있다가, 요청하는 방식대로 꺼내주는 직원에 가깝습니다.
마무리
이 문제는 코드 길이는 짧지만, 사고 방식은 꽤 깊습니다. 단순히 정답만 외우기보다 왜 pair를 함수로 표현할 수 있는지를 이해하면, 클로저와 고차 함수 개념이 훨씬 잘 잡힙니다.
정리하면, cons(a, b)는 두 값을 저장한 자료구조를 반환하는 것이 아니라 두 값을 기억하는 함수를 반환합니다. 따라서 car와 cdr는 각각 원하는 값을 꺼내는 함수를 전달해서 구현할 수 있습니다.