한빛미디어

IT/모바일

함수형 프로그래밍(functional programming)이란?

한빛미디어

|

2021-01-06

by Mike Loukides

원하는 만큼 함수적이 되어라.

함수형 프로그래밍(functional programming)은 본질적으로 프로그래밍을 수학으로 간주하는 것이다. 함수형 프로그래밍의 많은 개념들은 알론조 처치(Alonzo Church)의 람다 대수(Lambda Calculus)가 기초가 되었는데 이는 현대 컴퓨터의 개념과 조금이라도 흡사했던 그 어떤 것보다 앞섰다. 하지만 컴퓨터의 실제 역사는 다르게 흘러간다. 컴퓨터가 처음 발명될 무렵, 존 폰 노이만(John von Neumann)의 아이디어는 처치(Church)보다 더욱 중요하게 여겨졌고 그에따라 초기 컴퓨터들의 설계에서부터 현재까지 많은 영향을 미쳤다. 폰 노이만의 생각은 “프로그램이란 명령들을 실행하기 위해 설계된 기계장치에서 실행되는 명령들의 목록”이라고 확고했다.
 
그렇다면 함수형 프로그래밍을 “수학으로 간주되는 프로그래밍”이라고 여기는 것에 대한 의미는 무엇일까? 폰 노이만은 수학자였고, 모든 프로그래밍의 근원지는 수학에서 찾을 수 있다. 함수형 프로그래밍이 수학적이라는 것은 무슨 의미이고 어떠한 수학과 관련되어 있다는 것일까?
 
사실 특정한 수학 분야와 관련되어 있다는 의미는 아니다. 람다 대수가 집합론(set theory)이나 논리수학, 범주론 등의 특정한 분야의 수학과 밀접한 관련이 있는 것은 사실이다. 하지만 프로그래밍의 기초가 되는 초등학교 수학의 수준부터 접근해보자. 다음은 자주 봤던 코드일 것이다.
 
i = i+1 # or, more simply  
i += 1  # or, even more simply   
i++     # C, Java, but not Python or Ruby
 
수학적으로, 이건 말이 안되는 표현이다. 등식이란 “참”을 내포하고있는 어떠한 두 개의 값에 관한 관계를 나타낸 것이다. i 는 오직  i 와 같을 수 있을 뿐,  i+1 과 같을 수는 없다.  i++ 와  i+=1 은 똑같이 말이 안될 뿐더러, 등식처럼 생기지도 않았다. “ i 는 어떠한 것과 같다”라고 정의를 내린 순간, “ i 는 다른 어떠한 것과도 같을 수 없게 된다. 변수들은 값이 바뀌는 것들이 아니라, 값을 모르는 불변의 값인 것이다.
 
불변성은 함수형 프로그래밍에서 매우 중요한 원칙이다. 변수를 정의하게 되면, 그것을 바꿀 수 없다. (다른 함수의 범위에서 새로운 변수를 만들 수는 있지만, 그건 전혀 다른 문제다.) 함수형 프로그래밍에서 “변수”들은, “불변수”라고 볼 수 있고, 이는 프로그래밍 할 때 아주 중요한 개념이다. 반복문(loop)을 형성하고자 할때 인덱스 변수 (index variables)를 사용하지 못한다는 의미일 뿐만 아니라, 반복문 안의 그 어떤 변수들도 중간에 수정할 수 없음을 의미한다.
 
이런 해결가능한 반복문을 생성할때의 문제를 떠나서도, 어떠한 비(非) 함수형 언어로 똑같은 효과를  볼 수 있는 코드를 쓰지 못할 이유가 없다. 그냥 단순히 변수를 선언할때,  final 이나  const 를 붙이기만 해도 된다. 길게 봤을 때, 함수형 프로그래밍은 언어의 기능적인 부분보다는 특정한 능력을 기르는 훈련에 가깝다. 함수형 언어가 특정한 룰을 강화하는데 도움을 주긴 하겠지만, 그러한 기능을 제공하지 않는 다른 현대의 컴퓨터 언어를 사용하더라도 함수형 프로그래밍의 룰을 이행하는 것에는 큰 무리가 없다.
 
함수형 프로그래밍에서 중요한 또 하나의 원칙은 함수가 “일급 객체”라는 것이다. 이것은, 함수를 사용하는데 최소한의 제약만을 둔다는 뜻이다. 또한, 이름이 없는 익명의 함수인 일명 “람다 함수”를 만들 수도 있다(람다 대수는 이름을 가질 필요가 없는 익명의 함수다). 파이썬에서는 이러한 코드를 써볼 수 있다.
 
  data.sort(key=lambda r: r[COLUMN])
 
여기서 “key”는 배열의 특정 column으로 리턴(return)하는 익명 함수이며, 그 후 정렬(sorting)에 쓰인다. 개인적으로 익명 함수를 그다지 좋아하지 않는다. 익명 함수를 평범한, 알고있는 함수로 쓰는 것이 좀 더 명확할 때가 있기에 오히려 이렇게 써볼 것이다.
 
  def sortbycolumn(r): return r[COLUMN]
 data.sort(k=sortbycolumn)
 
함수를 함수의 argument로 사용할 수 있는 능력은 “전략 패턴, strategy pattern”을 구현할 수 있는 아주 좋은 방법이다:
 
  def squareit(x):   return x*x
  def cubeit(x):     return x*x*x
  def rootit(x):     import math; return math.sqrt(x)
  def do_something(strategy, x) ...
 
  do_something(cubeit, 42)
  weird = lambda x : cubeit(rootit(x))
  do_something(weird, 42)
 
가끔 모든 프로그래머들이 함수형 프로그래밍에서 진정으로 원하는 것은 바로 일급 객체 함수와 람다임을 느낀다. 파이썬에서는 람다가 초기 단계(1.0)에 추가되었지만 Java의 경우 Java 8까지 기다려야했다.
 
수학적으로 생각하는 것의 또 다른 (어쩌면 더 중요할 수도 있는)결과는 함수는 부작용이 있을 수 없으며, 같은 맥락으로 항상 동일한 값이 리턴된다. 만약 수학자(아니면 삼각함수를 마스터한 고등학생이)가 이렇게 작성해본다면,
 
  y = sin(x)
 
갑자기  sin(x가 전역변수(global variable)를 42로 설정하거나 호출(call)될 때마다 다른 값을 반환할 가능성에 대해서 생각할 필요가 없다. 절대 일어나지 않을 일이다. 수학에서는 부작용이라는 것은 의미 없다.  sin(x) 로부터 나오는 모든 정보는 리턴값 안으로 고정되어있다. 대부분의 프로그래밍 언어는 부작용이 너무도 쉽게 일어날 뿐더러 강박관념에 가까울 정도다. 다시 말해, 부작용 없는 함수를 만드는 것은 규율을 만드는 개념이다. 프로그래밍 언어가 열심히 규율을 만들 수 있지만 규율과 상관 없이 그냥 따르면 된다. 옆에서 “부작용 가자, 아무도 모를 거야!”라고 속삭이는 만화 영화에서나 나오는 악마도 없는데 말이다.
 
함수형 언어는 부작용의 부족함의 정도가 달리된다. 순수주의자에겐 현실 세계와 상호작용하는 모든 것이 부작용일 것이다. 종이 한 장 인쇄하기, 데이터베이스에서 로우(row) 바꾸기, 유저 스크린에 값 표시하기 등은 위에서 언급한 “함수에 의해 리턴값 안으로 고정되어 있다” 외의 부작용이며 Haskell의 모나드와 같은 메커니즘을 사용하여 숨겨져야 한다. 바로 이 부분이 많은 프로그래머들이 혼란스러워하고 두 손 들고 포기하는 시점이다. Java와 파이썬 둘 다에서는 람다 함수가 부작용이 생길 수 있으며, 엄밀히 말하면 사실 “함수적”이지 않다. 파이썬에 람다가 추가된 것에 대한 파이썬의 창시자 Guido van Rossum의 논의를 한번 읽어보면 좋을 것이다. 무엇보다도 그는 “사람들이 무엇을 말하고 생각하든, 파이썬이 함수형 언어의 영향을 이렇게 많이 받을 지 몰랐다”라고 말한다.
 
Streams는 종종 함수형 언어와 연관되어 있다. 기본적으로 느릿느릿 평가되는 긴(거의 무한한) 리스트들이다. 즉, 문자열의 요소가 필요한 경우에만 평가된다는 의미다. Maps는 리스트의 모든 요소에 함수를 적용하여 (이러한 목적을 위해)전문화된 리스트인 스트림을 포함한 새로운 리스트를 리턴한다. 이는 엄청나게 유용한 기능이다. 반복문(loop)을 작성할 필요 없이, 그리고 심지어 데이터를 얼마나 가지고 있는지조차 모르는 상태에서 반복문을 작성하는 최고의 방법이다. 또한 스트림 요소를 아웃풋에 패스할지 여부를 선택하는 “filter”를 만들고 maps와 filter를 함께 연결할 수도 있다. Unix pipeline이 생각난다면 맞다. Streams, maps, filters와 이 모든 걸 함께 연결하는 것은 함수형 언어와 Unix shell이 연관되어 있듯이 마찬가지로 연관이 있다.
 
반복문을 피하는 또 다른 방법은 파이썬의 기능인 “comprehensions(컴프리헨션)”를 사용하는 것이다. List comprehension(리스트 컴프리헨션)과 익숙해지기 쉽다. 컴팩트하고 하나하나의 오류를 제거하며 탄력적이다. 컴프리헨션이 전통적인 반복문에서는 컴팩트한 표기법처럼 보일 수 있지만, 사실 집합론에서 비롯되었으며 가장 가까운 컴퓨터적 “친척”은 함수형 프로그래밍이 아닌 관계형 데이터베이스(relational database)다. 다음은 리스트의 모든 요소에 함수를 적용하는 컴프리헨션이다.
 
  # pythonic examples.  First, list comprehension 
  newlist = [ somefunction(thing) for thing in things ] 
 
전통적인 반복문을 피하는 가장 일반적인 방법은 재귀(recursion)이다. 바로 자기 자신을 호출하는 함수다. 다음은 이전 컴프리헨션과 동등한 재귀다.
 
def iterate(t, l) :
    if len(t) == 0 : return l     # stop when all elements are done
    return iterate(t[1:],l + [somefunction(t[0])]) # process remainder
 
재귀는 함수형 언어의 주축이다. 수정되는 인덱스도 없고, resulting list를 수정할 필요도 없다(append가 수정에 포함 안될 시).
 
하지만 재귀에도 문제가 있다. 재귀함수를 이해하기란 어려운데 이는 당신이 당신만의 “부기(簿記), bookkeeping”를 만들어야 할 뿐만 아니라(이 경우, 벡터를 패스하여 결과가 리턴될 수 있도록 하는 것) 한 가지 흔한 “꼬리 재귀(tail recursion)”의 경우를 제외하고는 퍼포먼스 악몽이 되기 때문이다.
 
이 글에서 함수형 프로그래밍은 “수학”으로 간주된다고 시작했다. 적어도 부분적으로 맞는 말이다. 하지만 이게 과연 유용한가? 프로그래밍 개념에 다른 방식으로 매핑되는 많은 수학 분야들이 있다. 위상 수학자(기하학자)라면 그래프 데이터베이스들을 더 선호할 것이다. 하지만 어떤 수학 분야가 어떤 프로그래밍과 부합한지에 대한 토론은 도움이 딱히 안된다. 고등학교 시절 수학은 불변성(immutability), 무상태(statelessness)나 부작용의 부재를 다룰 때 도움이 될 수 있다. 하지만 프로그래머 대부분은 함수형 프로그래밍의 실제 수학적 기원에 대해 쳐다도 안 볼 것이다. 람다는 훌륭하다. 메소드 호출에서 argument와 같은 기능은 아주 훌륭하다. 심지어 재귀 역시(가끔은) 마찬가지다. 하지만 프로그래머들이 Java를 Haskell처럼 사용할 것이라고 생각 든다면 우리 스스로를 속이는 것이다. 그래도 괜찮다. Java 프로그래머들에게 람다의 가치는 “함수적”이라는 어떤 수학적 개념이 아니라 이름모를 내부 클래스보다 훨씬 더 큰 발전을 한다는 것이다. 함수적인 툴을 많기에 골라 사용하면 된다.
 
대학교에서, 필자는 엔지니어링은 절충안(trade-off)을 만들어내는 것이라고 배웠다. 그때부터 아주 극소수의 프로그래머들이 이 개념에 대해 언급한다고 들었다. 하지만 이런 절충안은 아직 좋은 엔지니어링의 중심이다. 엔지니어링에서 수학이 많이 사용되기는 하지만 엔지니어링은 수학이 아니다. 이는 수학에선 절충안을 만들어낼 필요가 없다는 점에서 기인할 것이다. 특정한 코딩 스타일을 위해 수학을 사용하는 것은, 특히 이러한 스타일이 버그를 줄여준다면, 상당히 유용할 것이다. 또한 수학적 툴을 쓸모있게 사용해 퍼포먼스와 실현 가능성 등 여러가지 요소들 사이에서 좋은 절충안을 발견해 새로운 방향점을 잡을 수도 있을 것이다.
 
*****
원문 : What is functional programming?
번역 : 김정욱

관련콘텐츠

댓글 입력