하스켈 함수

함수 언어

하스켈은 함수 언어입니다. 함수 언어의 대표적인 특징은 함수를 다른 함수에 인자로 넘기거나 함수가 다른 함수를 리턴하는 일이 가능하다는 것입니다. 이런 함수를 고차 함수(higher order function)라고 부릅니다. 이 글에서는 이런 특징을 가진 하스켈의 함수에 대해 살펴봅니다.

함수 호출

하스켈에서 함수 호출(function application)은 f a입니다. 여기서 f는 함수, a는 인자를 의미합니다. 함수 호출이 빈도가 높기 때문에 가장 간결한 문법을 사용하고 있습니다. 명령형 언어가 대개 f(a)와 같이 괄호를 사용하는 것과는 대비됩니다.

간단한 함수 호출의 예는 아래와 같습니다. isLower 함수는 인자 'a'를 입력으로 받아True를 값으로 돌려줍니다. 인자 'A'을 입력하면 False를 값으로 돌려줍니다. isLower함수는 Data.Char 모듈에 선언되어 있기 때문에 함수를 사용하기 위해서는 먼저 import구문을 사용해 임포트를 해주어야 합니다.

> import Data.Char
> isLower 'a' 
True
> isLower 'A'
False

또 다른 예로 fstsnd 함수가 있습니다. 둘 다 튜플을 인자로 받아 fst는 튜플의 첫 번째 요소를 리턴하고, snd는 튜플의 두 번째 요소를 리턴합니다.

> fst ('a', 1)
'a'
> snd ('a', 1)
1

함수의 인자 개수

하스켈 함수의 인자 개수는 항상 하나입니다. fstsnd 함수는 인자 'a'1 두 개의 인자를 받는 함수가 아니라 ('a', 1)이라는 튜플 값 하나를 인자로 받는 함수입니다.

하스켈에는 인자를 2개 이상 받는 함수가 존재하지 않습니다. 2개 이상의 인자가 필요하면 튜플을 넘기거나, 조금 있다 살펴볼 커리 함수(curried function)을 사용합니다.

함수의 타입

ghci에서 :t 명령을 이용하면 주어진 값의 타입을 확인할 수 있습니다. 아래 예를 보면,'a'의 타입은 Char임을 확인할 수 있습니다.

> :t 'a'
'a' :: Char

마찬가지 방법으로 함수의 타입도 확인할 수 있습니다. 앞서 살펴본 isLower 함수의 타입을 확인해보면 다음과 같은 결과가 출력됩니다.

> :t isLower
isLower :: Char -> Bool

isLower 함수의 타입이 Char -> Bool임을 확인할 수 있는데, 여기서 ->는 함수 타입을 나타냅니다. ->의 왼쪽에 있는 타입은 함수의 인자의 타입이고, ->의 오른쪽에 있는 타입은 함수의 리턴 타입입니다. 즉, isLower의 인자 타입은 Char, 리턴 타입은 Bool입니다.

같은 방법으로 fst의 타입도 확인할 수 있습니다.

> :t fst
fst :: (a, b) -> a

fst의 인자 타입은 (a, b), 리턴 타입은 a입니다. 튜플 타입은 (,)로 표현하는데, a타입을 첫 번째 요소로, b 타입을 두 번째 요소로 가지는 튜플 타입을 뜻합니다.

앞서 isLower 함수와 달리 구체적인 타입이 아닌 ab 같은 소문자로 표시된 타입은 다른 타입으로 대체될 수 있기 때문에 Char와 같은 타입과 구분하여 타입 변수(type variable)라고 합니다.

예를 들어, 튜플 ('x', 1::Int)(Char, Int) 타입을 가지는데 이 튜플로 fst 함수를 호출하면 fst 함수의 인자 타입 aChar로, bInt로 대체되어 fst의 타입은(Char, Int) -> Char가 됩니다. 같은 방식으로 (False, 'y') 튜플을 호출하면 aFalse의 타입인 Bool, b'y'의 타입인 Char가 되어 fst의 타입은(Bool, Char) -> Bool이 됩니다. 이처럼 하나의 함수가 인자에 따라 여러 타입을 가질 수 있는데, 이런 함수를 다형 함수(polymorphic function)라고 합니다.

연산자

하스켈은 +, -, &&, ||와 같이 특수 문자로 된 함수도 제공합니다. 이런 함수들을 연산자(operator)라고 부르는데, 연산자와 일반 함수의 차이점은 연산자는 infix로 일반 함수는 prefix로 사용한다는 점입니다. 즉, + 1 2가 아니라 1 + 2와 같은 형태로 함수를 호출하게 됩니다.

> 1 + 2
3

하지만 + 연산자를 prefix로 호출할 수도 있습니다. (+)와 같이 연산자를 괄호로 싸주면 일반 함수와 마찬가지로 prefix로 호출이 가능합니다.

> (+) 1 2
3

커리 함수(curried function)

앞서 하스켈 함수는 항상 인자를 하나만 받는다고 이야기하였습니다. 2개 이상의 인자가 필요할 경우, 튜플을 넘길 수 있다는 사실도 배웠습니다. 하지만 논리 연산자 (&&)의 예를 보면 인자를 2개 받는 것처럼 보입니다.

> False && True
False

분명 하스켈 함수는 인자를 1개만 받을 수 있다고 이야기했는데, (&&)는 어떻게 2개의 인자를 받을 수 있는 걸까요? :t 명령을 이용해서 (&&) 함수의 타입을 확인해 보겠습니다.

> :t (&&)
(&&) :: Bool -> Bool -> Bool

(&&)의 타입은 Bool -> Bool -> Bool입니다. 이 타입을 어떻게 해석해야 할까요? 앞서->는 함수 타입이고 ->의 왼쪽은 함수의 인자, ->의 오른쪽은 함수의 리턴 타입이라고 이야기했었습니다. 하지만 ->가 2개 있기 때문에 어디가 인자 타입이고, 어디가 리턴 타입인지 구분하기가 어렵습니다.

->은 우결합(right-associative)하기 때문에 Bool -> (Bool -> Bool)와 같이 괄호로 묶을 수 있습니다. 이 타입을 놓고 다시 해석을 해보면, (&&) 함수는 인자가 Bool 타입이고, 리턴 값은 함수 타입인 Bool -> Bool임을 알 수 있습니다.

바꿔 말해, (&&) 함수는 인자 2개를 받는 함수가 아니라 Bool 타입의 인자를 받아서Bool -> Bool 타입의 함수를 리턴하는 함수입니다. 하스켈은 고차 함수를 지원하는 언어이기 때문에 함수가 함수를 인자로 받거나 함수를 리턴하는 것이 가능합니다.

실제로 인자를 하나만 넘겨보겠습니다.

> (&&) False
Prelude> (&&) False

<interactive>:5:1: No instance for (Show (Bool -> Bool)) (maybe you haven't
applied enough arguments to a function?) arising from a use of ‘print’ In a stmt
of an interactive GHCi command: print it

에러 메시지는 ghci가 함수는 화면에 제대로 출력할 수 없기 때문에 발생합니다. 정말 함수를 리턴한 것이 맞는지는 :t 명령으로 확인할 수 있습니다.

> :t (&&) False
(&&) False :: Bool -> Bool

이처럼 리턴된 값이 Bool -> Bool 타입을 가진 함수라는 사실을 확인할 수 있습니다. 이 함수를 f라는 이름으로 바인딩하여 다시 True를 호출해 보겠습니다.

> let f = (&&) False
> f True
False

False && True(&&) 함수에 2개의 인자 FalseTrue를 넘긴 것이 아니라, 먼저False를 호출하고 그 결과로 함수가 리턴되면 다시 True를 인자로 리턴된 함수를 호출한 것입니다. 이 과정을 좀 더 명시적으로 보기 위해 괄호를 추가하면 다음과 같습니다.

> ((&&) False) True
False

이렇게 함수가 함수를 리턴하는 방식으로 여러 개의 인자를 받을 수 있게 만든 함수를 커리 함수(curried function)라고 부릅니다. 이와 달리 여러 값을 하나의 튜플로 묶어 넘기는 것을 언커리(uncurried function) 함수라고 부릅니다. (&&)는 커리 함수의 예이고, 앞서 살펴본 fstsnd 함수는 언커리 함수의 예입니다.

섹션

(+), (-)와 같은 연산자들은 모두 커리 함수이기 때문에 인자를 하나만 줘서 호출하면 함수를 리턴합니다. 예를 들어, (+) 1은 주어진 인자에 1을 더해서 리턴하는 함수가 됩니다.

> let f = (+) 1
> f 2
3

이렇게 바이너리 연산자에 인자 하나만 줘서 새로운 함수를 만드는 경우가 흔하기 때문에 하스켈은 (+1)과 같은 특별한 문법을 제공합니다. 이렇게 연산자에 인자 하나만 줘서 새로운 함수를 만드는 방법을 섹션(section)이라고 부릅니다.

> (+1) 2
3

피연산자의 위치에 따라 섹션을 만드는 방법은 2가지가 있습니다. 예를 들어 (1-)는 1에서 주어진 값을 빼는 함수이고, (-1)는 주어진 값에서 1을 빼는 함수입니다.

> (-5) 3
-2
> (5-) 3
2

하스켈 입문서

Learn You a Haskell for Great Good!

보통 줄여서 LYHGG라고 부르는 Learn You a Haskell for Great Good!는 가장 쉬운 하스켈 입문서입니다. 원서는 무료로 공개되어 있어 온라인으로 읽으실 수 있고, 국내에는가장 쉬운 하스켈 책 느긋하지만 우아하고 세련된 함수형 언어이란 제목으로 번역 출간되었습니다.

책 전반부에서는 하스켈 타입, 함수, 문법, 재귀, 고차 함수, 모듈 등 하스켈 언어의 기본적인 내용을 쉽고 간결하게 설명하고 있고, 후반부에는 타입클래스, 입출력, Functor, Applicative Functor, Monoid, Monad 등 고급 주제들을 다루고 있습니다.

참고로 쉬운 입문서라고는 하지만 책 후반부의 난이도가 전반부에 비해 상대적으로 높아서 독학하시는 분들이 대개 Functor나 Monad에 막혀서 그만 두는 경우가 많습니다.

Real World Haskell

LYHGG가 주로 하스켈 언어의 특징에 대해 소개한 책이라면, Real World Haskell은 하스켈을 실제 업무에 사용할 수 있도록 다양한 예제를 통해 하스켈의 특징을 설명하고 있습니다. 저자인 Bryan O’Sullivan과 Don Stewart는 각각 페이스북과 스탠다드 차타드 은행에서 하스켈 팀을 이끌고 실제로 하스켈로 개발 업무를 수행하고 있습니다.

덕분에 이 책에는 JSON 처리, 파서 작성, 시스템 프로그래밍, 데이터베이스, GUI, 네트워크 프로그래밍 등의 하스켈을 실제 업무에 사용하기 위해 필요한 구체적인 지식들이 포함되어 있습니다.

한 가지 아쉬운 점은 2007년 이 책이 출간된 이후 하스켈 라이브러리에도 여러 변경 사항이 생기면서 일부 챕터의 코드는 더 이상 제대로 동작하지 않는다는 점입니다. 특히, 챕터 19 “Error handling”의 경우 하스켈의 예외 처리 라이브러리가 완전히 개정되면서 더 이상 유효하지 않게 되었습니다.

원서는 온라인에서 무료로 읽으실 수 있고, 아쉽게도 국내에 번역 출간된 적은 없습니다.

Haskell Book

Haskell Book은 요즘 하스켈 커뮤니티에서 가장 핫한 책입니다. 이 책은 Christopher Allen과 Julie Moronuki이 공동 집필하였는데, 특이한 점은 Julie Moronuki은 이 책을 쓰기 전까지 개발자가 아니었다는 사실입니다. 대학에서 철학을 전공하고 교사 및 사서로 일하던 Julie는 Christopher의 설득으로 하스켈을 생애 첫 프로그래밍 언어로 배웠고 그 내용을 함께 정리하여 책으로 출간하게 되었습니다. 덕분인지 기존의 하스켈 책보다 훨씬 이해하기 쉽게 잘 작성되었다는 평가를 받고 있습니다.

이 책의 1장은 하스켈이 아닌 람다 대수(lambda calculus)로 시작합니다. 람다 대수는 하스켈을 비롯한 모든 함수 언어의 이론적 배경이 되는 수학으로 함수 언어를 제대로 사용하려면 언젠가는 배워야 하는 내용인데, 이 책은 과감하게 1장에서 람다 대수를 설명하고 2장부터 하스켈에 대한 소개로 이어집니다.

이 책은 LYHGG와 Real World Haskell의 장점을 고루 섞어놓았습니다. 하스켈 언어에 대한 기본 설명도 충실하고, 배운 내용을 실제로 사용할 수 있게 여러 실례도 소개하고 있습니다. 새로 하스켈을 공부하시는 분이라면 제가 가장 추천 드리는 책이기도 합니다.

이 책은 아직 완간되지 않습니다. 대신 홈페이지에서 $59에 Early Access 버전을 구매하실 수 있습니다. 당연히 아직 한글 번역서도 존재하지 않습니다. 국내 출판사에서 번역 출간 계약을 하면 좋겠습니다.

하스켈 무한 리스트

하스켈은 유한한 리스트뿐만 아니라 무한히 긴 리스트를 정의할 수 있습니다. 예를 들어,[m..]은 m에서 시작하는 모든 정수의 리스트가 됩니다.

> [1..]
[1,2,3,4,5,6,7,8Interrupted.
>

ghci[1..]을 입력하면 이 리스트를 화면에 출력하는데, 무한히 긴 리스트이므로 끝이 나지 않고 계속해서 숫자를 출력하는 것을 보실 수 있습니다. Ctrl+C 키를 눌러서 종료하면 Interrupted 메시지와 함께 출력이 종료됩니다.

리스트 기초에서 살펴본 take 함수를 이용하면 무한히 긴 리스트에서 일부 원소만 뽑아낼 수 있습니다.

> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]

같은 리스트를 [1..]과 같은 표기법을 이용하지 않고 재귀적으로 정의할 수도 있습니다. 이렇게 정의한 ints 리스트에서 take 함수로 10개 처음 10개 원소를 꺼내보면 [1..]과 마찬가지로 1부터 10까지 정수의 리스트를 리턴하는 것을 볼 수 있습니다.

> let ints = 1 : map (+1) ints
> take 10 ints
[1,2,3,4,5,6,7,8,9,10]

ints가 어떻게 동작하는지를 이해하기 위해 원소를 차례대로 하나씩 꺼내보겠습니다.ints는 머리 1과 꼬리 map (+1) ints로 구성된 리스트이기 때문에 ints의 첫 번째 원소는 1입니다. 두 번째 원소는 map (+1) ints의 머리입니다. map (+1)은 주어진 리스트의 모든 원소에 1을 더해주는데, 여기서는 다시 ints를 인자로 넘겼습니다. 이 ints의 첫 번째 인자는 1이였으므로 map (+1) ints의 첫 번째 원소는 11을 더한 2가 됩니다. 따라서 전체 ints의 두 번째 인자도 2가 됩니다. 같은 방법으로 세 번째 원소를 꺼내면 3이 되는 것을 확인할 수 있습니다.

이런 무한 리스트도 유한한 리스트와 마찬가지로 map이나 filter 같은 함수들을 사용해 원소를 원하는 대로 바꿀 수 있습니다. 아래 예제에서 evensints 리스트의 각 원소에2를 곱한 짝수의 리스트입니다. take 함수로 10개를 뽑아보면 2부터 20까지 짝수 10개가 리턴되는 것을 볼 수 있습니다.

> let ints = 1 : map (+1) ints
> let evens = map (*2) ints
> take 10 evens
[2,4,6,8,10,12,14,16,18,20]

조금 더 간단한 예로 1이 무한히 반복되는 리스트를 정의해 볼 수도 있습니다.

> let ones = 1 : ones
take 10 ones
[1,1,1,1,1,1,1,1,1,1]

ones의 머리는 1이고 꼬리는 다시 ones입니다. 따라서 ones의 두 번째 원소는 ones의 머리이므로 1이고, 세 번째, 네 번째 원소도 모두 1이 되는 것을 알 수 있습니다.

하스켈은 무한 리스트를 정의하는 경우가 많으므로 Prelude 모듈에서는 편의를 위해iterate를 함수를 제공합니다. iterate 함수는 함수 f를 인자 x에 0번, 1번, 2번, … 반복해서 호출하여 리스트를 만들어줍니다.

iterate f x == [x, f x, f (f x), ...]

iterate 함수를 이용해서 ints를 정의할 수도 있습니다.

> let ints = iterate (+1) 1

iterate (+1) 1[1, (+1) 1, (+1) $ (+1) 1, (+1) $ (+1) $ (+1) 1, ...]와 동일하고,(+1) $ (+1) 1은, (+2) 1과 같으므로 위 리스트는 [1..]과 같다는 사실을 쉽게 확인할 수 있습니다.

iterate 함수로 ones도 정의할 수 있습니다.

> let ones = iterate id 1

여기서는 iterate 함수의 인자로 id를 넘겼습니다. idSKI 콤비네이터에서 살펴보았던 항등 함수(identity function)로 인자를 받아 그대로 리턴합니다. iterate id 1[1, id 1, id $ id 1, id $ id $ id 1, ..]이므로 모든 원소가 1이 되는 것을 확인할 수 있습니다.

이런 경우에는 repeat 함수를 이용하면 더 간단합니다. repeat 함수는 주어진 인자 x를 무한히 반복합니다.

> let ones = repeat 1

이런 무한 리스트는 zip 함수와 같이 사용하는 경우가 많습니다. zip 함수는 리스트 두 개를 인자로 받아 각 리스트이 원소를 하나씩 뽑아 묶은 튜플의 리스트를 리턴합니다. 간단한 예를 보면,

> zip [1,2,3,4,5] ['a', 'b', 'c', 'd', 'e']
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]

두 리스트 중 한 쪽이 짧으면 먼저 끝나는 쪽을 기준으로 끝이 납니다.

zip [1,2,3,4,5] ['a', 'b', 'c']
[(1,'a'),(2,'b'),(3,'c')]

zip 함수를 이용하여 리스트 각 원소의 인덱스를 매기고 싶은데, 리스트이 길이를 미리 알 수 없다면 어떻게 해야 할까요? 이런 경우 [1..]와 같이 무한히 긴 인덱스의 리스트를 넘기면 됩니다.

> let ls = ['a', 'b', 'c', 'd', 'e']
> zip [1..] ls
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]

하스켈 검색 엔진 Hoogle

구글(Google)과 유사한 발음의 Hoogle은 하스켈 API 검색 엔진입니다. API의 이름뿐만 아니라 타입으로도 검색할 수 있다는 점이 가장 큰 특징입니다.

예를 들어 (a -> b) -> [a] -> [b] 타입의 함수를 찾고 싶으면 검색 창에(a -> b) -> [a] -> [b]라고 입력해주면 됩니다. 그러면 Hoogle은 검색 결과로 PreludeData.List 모듈에 정의된 map 함수를 보여줍니다.

map :: (a -> b) -> [a] -> [b]

cabal-installhoogle 패키지를 설치하면 커맨드라인에서도 검색이 가능합니다.

$ cabal install hoogle

->가 포함된 타입을 검색할 때는 검색어를 ""로 묶어줍니다.

$ hoogle "(a -> b) -> [a] -> [b]"
Prelude map :: (a -> b) -> [a] -> [b]
Data.List map :: (a -> b) -> [a] -> [b]
...

~/.ghc/ghci.conf 파일에 다음 내용을 추가해주면 ghci 내에서도 검색이 가능합니다.

:def hoogle \s -> return $ ":! hoogle --count=15 \"" ++ s ++ "\""

ghci에서 검색할 때는 :hoogle 뒤에 검색어를 입력하면 됩니다.

>:hoogle (a -> b) -> [a] -> [b]
 Prelude map :: (a -> b) -> [a] -> [b]
 Data.List map :: (a -> b) -> [a] -> [b]
 ...

하스켈 리스트와 고차함수

앞서 리스트 기초 장에서는 리스트 타입의 정의와 head, tail, length 등 기본적인 리스트 함수들을 살펴보았습니다. 리스트는 이런 기본적인 함수 외에도 map, filter, foldr처럼 함수를 인자로 받는 함수들을 제공합니다. 하스켈에서는 함수가 다른 함수를 인자로 받거나 함수를 리턴할 수 있는데 이런 특성을 가진 함수들을 고차 함수(higher-order function)라고 부릅니다.

map 함수

우선 대표적인 고차 함수의 예로 map을 살펴보겠습니다. map의 타입은(a -> b) -> [a] -> [b]입니다. 타입 a를 받아 b를 리턴하는 함수 하나와 a 타입의 리스트 하나를 인자로 받아 b 타입의 리스트를 리턴하는 함수입니다. map은 주어진 함수를 리스트 각각의 원소에 적용하여 새로운 리스트를 리턴합니다. 간단한 사용 예는 다음과 같습니다.

> map (\x -> x + 1) [1,2,3]
[2,3,4]

여기서 \x -> x + 1은 하스켈에서 람다 함수(lambda function)을 정의하는 방법입니다.\를 사용하는 이유는 아스키 코드 중에서 그리스 알파벳 람다(λ)를 가장 닮았기 때문입니다.

이 람다 함수는 인자 x를 받아서 x + 1을 리턴합니다. ->를 기준으로 왼쪽은 인자, 오른쪽은 함수 몸통(function body)입니다. 인자가 2개 이상인 경우는 공백 문자로 구분해 주면 됩니다. 따라서 인자 xy를 받아 합을 리턴하는 함수는 \x y -> x + y로 정의할 수 있습니다.

앞서 하스켈 함수에서 배운 섹션(section)을 사용하면 같은 함수를 좀 더 간결히 표현할 수 있습니다. \x -> x + 1은 2개의 인자를 받는 (+) 연산자에 인자 하나만 먼저 적용하여 새로운 함수를 만들었다고 볼 수 있기 때문에 (+1)과 같이 표현할 수 있습니다.

> map (+1) [1,2,3]
[2,3,4]

filter

filter 함수는 각 원소에 대해 Bool을 리턴하는 테스트 함수(predicate)를 인자로 받아 각 원소 중에서 테스트를 통과한 원소들만 모아 새로운 리스트로 리턴하는 함수입니다.filter의 타입은 (a -> Bool) -> [a] -> [a]인데 첫 번째 인자 타입 a -> Bool을 보면 테스트 함수는 리스트의 원소 타입 a를 인자로 받아 Bool을 리턴하는 것을 확인하실 수 있습니다.

> filter odd [1,2,3,4,5]
[1,3,5]

filter와 비슷한 함수로는 partition 함수가 있는데, filter와 마찬가지로 테스트 함수를 받아 리스트 각 원소에 대해 테스트를 실시하는데, filter는 테스트를 통과한 원소의 리스트를 리턴하는 반면 partition은 테스트를 통과한 원소와 통과하지 못한 원소를 따로 모아 각각을 리스트로 리턴합니다.

리턴값이 2개의 리스트이므로 튜플을 사용합니다. 튜플의 첫 번째 원소는 테스트를 통과한 원소들의 리스트이고, 튜플의 두 번째 원소는 테스트를 통과하지 못한 원소들의 리스트입니다. partition 함수는 filter와 달리 Prelude 모듈에 정의되어 있지 않으므로 사용하려면 먼저 Data.List 모듈을 임포트해주어야 합니다.

> import Data.List
> partition odd [1,2,3,4,5]
([1,3,5],[2,4])

foldr

마지막으로 살펴볼 함수는 foldr입니다. 리스트의 경우 foldr의 타입은(a -> b -> b) -> b -> [a] -> b입니다. 알서 살펴본 map이나 filter와는 달리 리턴 타입이 [a]가 아니라 b임을 볼 수 있습니다. map처럼 리스트 각 원소를 가지고 새로운 리스트를 만들거나, filter처럼 리스트의 각 원소 중 일부를 추려서 새로운 리스트를 리턴하는 함수는 아님을 짐작할 수 있습니다.

이해를 돕기 위해 먼저 사용 예를 살펴보겠습니다. 다음은 foldr을 이용해 리스트 원소들의 합을 구하는 코드입니다.

> foldr (+) 0 [1,2,3,4,5]
15

(+)는 인자 두 개를 받아 합을 리턴하는 함수이고, 0은 초기값입니다. 위 foldr 함수를 풀어써보면 다음과 같습니다.

(1 + (2 + (3 + (4 + (5 + 0 )))))

리스트 기초에서 살펴본 리스트 [1,2,3,4,5]의 정의와 비교해보면 모양이 상당히 유사하다는 사실을 알 수 있습니다.

(1 : (2 : (3 : (4 : (5 : [])))))

리스트 정의에서 (:)(+)로 바뀌었고, []0으로 바뀌었습니다. 즉, foldr f acc의 의미는 리스트를 펼쳐놓고 (:)f[]acc로 바꾼 것과 같다고 생각할 수 있습니다.

이번에는 각 리스트의 곱을 구해보겠습니다.

foldr (*) 1 [1,2,3,4,5]

마찬가지로 리스트를 정의에 따라 풀어 쓰고 (:)(*)[]1로 바꿔쓰면 됩니다.

(1 : (2 : (3 : (4 : (5 : [])))))
(1 * (2 * (3 * (4 * (5 * 1 )))))

foldr을 이용하면 리스트의 길이를 리턴하는 length 함수도 구현할 수 있습니다. 아래 리스트 정의에서 (:)[]를 어떻게 바꾸면 각 원소 대신에 리스트의 길이를 계산하게 될지를 생각해 봅시다.

(1 : (2 : (3 : (4 : (5 : [])))))

금방 답이 떠오르지 않으신가요? 이해를 돕기 위해 이번에는 (:) 연산자를 infix가 아닌 prefix로 표기해보겠습니다.

(:) 1 ((:) 2 ((:) 3 ((:) 4 ((:) 5 []))))

(:)를 보기 좋게 임의의 함수로 f로 대체하면 다음과 같습니다.

f 1 (f 2 (f 3 (f 4 (f 5 []))))

이제 함수 f를 어떻게 정의하면 리스트의 길이를 구할 수 있을까요? 길이를 구할 때 리스트의 원소가 무엇인지는 중요하지 않으므로 첫 번째 인자를 무시합니다. 그리고 f 함수가 매번 호출될 때마다 리스트의 길이가 1만큼 늘어난다는 사실에 착안하여 두 번째 인자로 넘어온 값에 1을 더해주면 어떨까요? 그렇게 f를 정의하면 \x y -> y + 1가 되는데, 실제로 테스트해보면 리스트의 길이를 리턴하는 것을 볼 수 있습니다.

> foldr (\x y -> y + 1) 0 [1,2,3,4,5]
5

foldr 함수는 맥가이버 칼로 불릴 만큼 굉장히 강력한 함수입니다. foldr 함수를 이용하면 map이나 reverse 함수도 구현할 수 있습니다.

숙제: foldrmapreverse를 구현해 봅시다.

하스켈 리스트 기초

리스트는 하스켈에서 가장 범용적으로 사용되는 데이터 타입입니다. 리스트의 타입은[a]로 표시하는데, a는 요소 타입을 뜻합니다. 예를 들어 Char의 리스트는 [Char],Int의 리스트는 [Int]와 같이 표시합니다.

리스트 [a]머리(head)꼬리(tail)로 구성되고 머리는 타입 a, 꼬리는 또 다른 리스트 [a] 타입을 가집니다. 구성 요소에 자기 자신의 타입을 반복되기 때문에 리스트와 같은 타입을 재귀 타입(recursive type)이라고 부릅니다.

머리와 꼬리를 이용하여 이용하여 리스트를 만드는 연산자는 (:)입니다. []는 아무 요소도 없는 빈 리스트를 의미합니다. 리스트를 새로 만들려면 일단 리스트가 하나 있어야 하기 때문에 맨 처음에는 빈 리스트를 이용합니다. 1[]을 합하면 리스트 [1]이 됩니다.

> 1 : []
[1]
> 1 : 2 : []
[1,2]
> 1 : 2 : 3: []
[1,2,3]

여기서 (:) 연산자는 우결합(right-assocative)하기 때문에 1:2:3:[]의 의미는(1:(2:(3:[])))과 같습니다. 먼저 3[]를 합하여 [3]을 만들고 다시 여기에 2를 합하여 [2,3]를 만들고 다시 여기에 1을 합하여 [1,2,3]을 만든다고 이해하시면 됩니다.

매번 이렇게 리스트를 생성하면 불편하기 때문에 조금 더 쉽게 리스트를 생성하는 방법이 있습니다. [] 안에 ,로 구분하여 요소들을 나열해주면 됩니다.

> [False, True, False]
[False,True,False]
> [1,2,3]
[1,2,3]
> []
[]

length 함수는 리스트의 요소 개수를 리턴합니다.

> length [1, 2, 3]
3
> length [1]
1
> length []
0

참고로 우리가 쓰고 있는 String 타입은 실제로는 Char 타입의 리스트인 [Char]로 정의되어 있습니다. 따라서 리스트를 인자로 받는 함수인 lengthString을 줘도 문자열의 길이를 리턴합니다.

> length "abc"
3
> length ""
0

head 함수는 리스트의 머리를 돌려줍니다. 빈 리스트를 인자로 받았을 경우에는 돌려줄 머리가 없으므로 에러가 납니다.

> head [1, 2, 3]
1
> head  []
*** Exception: Prelude.head: empty list

반대로 tail 함수는 리스트의 꼬리를 돌려줍니다. 마찬가지로 빈 리스트를 인자로 받았을 때는 돌려줄 꼬리가 없으므로 에러가 납니다.

> tail [1, 2, 3]
[2,3]
> tail []
*** Exception: Prelude.tail: empty list

(++) 연산자는 리스트 두 개를 합친 새로운 리스트를 리턴합니다.

> [1, 2, 3] ++ [4, 5, 6]
[1,2,3,4,5,6]
> [1, 2, 3] ++ []
[1,2,3]

reverse 함수는 리스트의 순서를 거꾸로 뒤짚어서 리턴합니다. 빈 리스트는 뒤짚을 요소가 없으므로 그대로 빈 리스트를 리턴합니다.

> reverse [1, 2, 3]
[3,2,1]
> reverse []
[]

sum이나 product 함수를 이용하면 리스트 요소들의 모두 더한 값이나 모두 곱한 값을 얻을 수도 있습니다.

> sum [1, 2, 3, 4, 5]
15
> product [1, 2, 3, 4, 5]
120

takedrop 함수를 이용하면 서브 리스트를 얻을 수 있습니다. take는 주어진 개수만큼의 요소를 새로운 리스트로 만들어 리턴합니다. drop은 주어진 개수만큼의 요소를 빼거 나머지 요소들을 새로운 리스트로 만들어 리턴합니다.

> take 3 [1, 2, 3, 4, 5]
[1,2,3]
> drop 3 [1, 2, 3, 4, 5]
[4,5]

이 외에도 Data.List 문서를 보시면 리스트에 정의된 수많은 함수들을 확인하실 수 있습니다. 리스트는 매우 자주 사용되는 데이타 타입이므로 리스트 함수들은 여러 번 공부하고 익혀서 숙지하고 계시면 좋습니다.

SKI 콤비네이터

때는 2030년, 2016년 바둑으로 이세돌을 이긴 알파고는 더욱 진화하여 인류를 지배하기 시작하였다. 저항군은 알파고를 파괴할 컴퓨터 바이러스 개발에 나선다. 알파고는 이를 막고자 개발자들이 사용할 수 있는 함수들을 파괴하기 시작했다. 오랜 싸움 끝에 이제 인류에게 남은 함수는 단 3개뿐, 과연 인류는 3개의 함수로 알파고를 물리칠 바이러스를 만들 수 있을까?

2030년 인류가 바이러스 개발에 사용할 수 있는 함수는 단 3개가 남았습니다. 남은 함수가 3개밖에 없다는 사실을 알고 이제 알파고를 이기는 것을 불가능하다며 많은 사람들이 절망하고 있습니다.

s x y z = x z (y z)
k x y = x
i x = x

각각 s, k, i라고 이름 붙여진 함수들은 보시는 바와 같이 아주 간단한 일을 하는 함수입니다.

  • i: 인자 x를 받아서 그대로 x를 리턴하는 함수입니다. 항등 함수를 뜻하는 영어 identity function의 i에서 따온 이름입니다.
  • k: 2개의 인자 xy를 받아서 항상 첫 번째 인자 x를 리턴하는 함수입니다. 상수 함수를 뜻하는 영어 constant function을 c에서 따온 이름인데, 개발자들은 c를 k라고 부르는 습성이 있어 k 함수가 되었습니다.
  • s: 3개의 인자 x, y, z를 받는 함수입니다. 먼저, z를 인자로 x를 호출합니다. 그리고 나서 리턴된 함수에 z를 인자로 y 함수를 호출한 결과를 다시 인자로 넘겨서 호출합니다. s라는 이름은 대체 연산자를 뜻하는 substitution operator의 s에서 왔습니다.

i 함수와 k 함수는 크게 쓸모가 없어 보입니다. 인자를 그대로 돌려주는 함수나 두 개의 인자 중 항상 첫 번째 인자만 리턴하는 함수 모두 그다지 유용한 일을 하는 것 같지는 않습니다.

그나마 s 함수는 뭔가 유용한 일을 하는 것 같습니다. s가 대체 연산자라는 이름이 붙은 이유는 함수 호출은 변수를 대체하는 것과 같기 때문입니다. 간단한 예로, 인자 x를 받아서 튜플 (x, x)를 리턴하는 함수 \x -> (x,x)가 있다고 하면 이 함수에 1을 인자로 호출한 (\x -> (x, x)) 1의 값은 변수 x를 1로 대체한 (1, 1)과 같기 때문입니다.

많은 사람들이 절망하고 있던 그 순간 그간 비주류 언어라며 천대 받던 하스켈 개발자가 나섭니다. 이 세 함수만 있으면 알파고를 무찌를 바이러스를 충분히 개발할 수 있다고 자신감 있게 주장합니다. 사람들은 동요하기 시작합니다. 누군가가 묻습니다. “하지만 우린TrueFalseif 문도 없는데 없다고! 조건문 없이 어떻게 프로그램을 만들 수 있단 말이야?”

하스켈 개발자가 답합니다. “가능합니다. Truek, Falsesk로 정의하면 됩니다.k 함수는 인자 x, y를 받아서 x를 리턴합니다. sk 함수는 반대로 인자 x, y를 받아서 y를 리턴합니다. True일 때는 x, False 일 때는 y를 리턴하는 값을 만들었으므로 if 문처럼 사용할 수 있습니다.”

k x y = x
s k x y = k y (x y) = y

또 다른 개발자가 외칩니다. “하지만 루프가 없다고! 루프 없이는 아무런 유용한 프로그램을 짤 수가 없다고. 튜링 머신이 아니란 말이야.” 하스켈 개발자가 단호한 표정으로 다시 답합니다. “우리에게 남은 함수는 세 개 밖에 없고, 말씀하신 것처럼 이제 인류는 더 이상 루프를 사용할 수가 없습니다. 하지만 우리에게 재귀 함수가 있습니다. 루프에 비해 느리고, 어렵다며 천대 받던 재귀 함수이지만 이제 우리에게 남은 유일한 희망입니다. 재귀 함수를 정의하는 원리는 간단합니다. 먼저 sii를 보시기 바랍니다.”

sii a = (i a) (i a) = a a

“이처럼 sii는 인자 a를 받아서 a를 인자로 자기 자신을 다시 호출합니다. 네. 맞습니다. 여러분이 사랑하고 동시에 증오하던 무한 루프입니다. 이런 방식을 이용하면 재귀 함수 y를 만들어낼 수 있습니다. y 함수는 다음과 같이 정의합니다.”

y = s(c(s(ks)k)(sii))(c(s(ks)k)(sii))

또 다른 개발자가 소리칩니다. “그럼 정수는? 루프나 if가 있다고 하도 정수 없이 무슨 코딩을 하란 말이야?” 하스켈 개발자는 동요하지 않고 여전히 자신감 있게 설명을 이어갑니다. 오랜 설명 끝에 결국 모든 개발자들이 s, k, i 단 3개의 함수만으로 알파고를 물리칠 바이러스 개발이 가능하다는 말을 납득합니다. 이후 더 이상 하스켈 개발자를 천대하지 않고 함께 열심히 바이러스를 개발하여 알파고를 무찌르고 행복하게 살았답니다.

여기서 설명한 s, k, i 함수는 컴퓨터 과학자, 논리학자에게는 이미 잘 알려진 SKI 콤비네이터입니다. 콤비네이터라는 용어 자체가 생소할 수 있는데, 콤비네이터는 free 변수가 없는 함수를 뜻합니다. 여기서 free 변수가 없다는 뜻은 함수에 주어진 인자만 사용하는 순수 함수를 말합니다. 다음은 콤비네이터의 예제입니다.

\a -> a
\a -> \b -> a
\f -> \a -> \b -> f b a

콤비네이터가 아닌 함수의 예제는 다음과 같습니다. x라는 변수가 함수의 인자로 정의되지 않았습니다. 이런 변수 x를 free 변수라고 부릅니다.

\a -> \b -> x

재미있는 사실은 계산가능성(computability)를 봤을 때 SKI 콤비네이터는 람다 대수(lambda calculus)와 동급이고, 람다 대수가 튜링 머신과 동급이기 때문에 결국 SKI 콤비네이터가 정의한 3개의 함수만 가지고 우리가 컴퓨터를 가지고 할 수 있는 모든 계산을 할 수 있습니다. 인류가 이 세 개의 함수만 가지고 알파고를 무찌를 바이러스를 만들 수 있었던 이유도 세 개의 함수가 튜링 머신과 동급이기 때문입니다.

사실 엄밀하게 이야기하면 sk 2개만 함수만 있어도 됩니다. i조차도 sk의 조합인 skk로 만들어 낼 수 있기 때문입니다.

skk a = k a (k a) = a

s, k, i 세 함수를 장황하게 소개한 이유는 각 함수가 하스켈 프로그래밍에도 실제로 아주 자주 사용되는 필수 함수이기 때문입니다. 이들 함수는 하스켈에서 각각((->) r) Applicative 인스턴스의 (<*>) 함수, const 함수, id 함수로 정의되어 있습니다. 앞으로 설명할 내용에도 자주 등장하는 함수들이 꼭 기억해 두시기 바랍니다.

타입 클래스 기초

하스켈 타입 클래스(type class)오버로딩을 지원하기 위한 방법입니다. 여기서 오버로딩은 하나의 심볼(함수 혹은 연산자)를 타입에 따라 여러 의미로 사용하는 것을 의미합니다.대표적인 예로 하스켈의 Show 타입 클래스를 살펴보겠습니다. Show 타입 클래스는 show라는 함수를 정의하고 있습니다. show 함수는 인자를 받아 그 인자를 String으로 변환한 결과를 리턴합니다. 아래와 같이 다양한 인자 타입에 대해서 잘 동작하는 것을 확인할 수 있습니다.

> show 1
"1"
> show False
"False"
> show "Hello World"
"\"Hello World\""

IntString으로 변환하는 방법과 BoolString으로 변환하는 방법은 다를 수 밖에 없는데, show는 어떻게 IntBool, String등 여러 타입의 인자를 모두 변환할 수 있는 것일까요? 궁금즘을 해결하기 위해 show의 타입을 살펴보겠습니다.

> :t show
show :: Show a => a -> String

타입을 보면 일반적인 함수와 달리 a -> String 함수 타입 왼쪽에 Show a =>라는 컨텍스트(context)가 추가되어 있는 것을 볼 수 있습니다. 여기서 => 앞에 나오는 Show aa라는 타입이 Show라는 타입 클래스를 구현한 타입이어야 한다는 뜻입니다.

Show 타입 클래스를 구현한다는 것이 어떤 의미인지를 알아보기 위해, Show 타입 클래스가 어떻게 정의되었는지 먼저 살펴보겠습니다. 다음은 Prelude 모듈에 정의된 Show의 정의입니다. (간단한 설명을 위해 showsPrec등 다른 함수들은 생략했습니다.)

class  Show a  where
   show      :: a   -> String

위 정의의 의미는 타입 aShow라는 타입 클래스의 인스턴스가 되기 위해서는a -> String이라는 타입을 가진 show 함수를 제공해야 한다는 뜻입니다. 여기서 Show타입 클래스는 Show 타입 클래스를 구현하는 타입들이 갖춰야 하는 일종의 인터페이스라고 이해할 수 있습니다.

우리가 Int, Bool, String 등의 인자에 대해 show 함수를 호출할 수 있는 이유는 각각의 타입에 대해 Show 타입 클래스 인스턴스가 미리 정의되어 있기 때문입니다. 인스턴스 정의는 다음과 같이 instance 키워드를 사용합니다.

instance Show Int where
    show = ...

instance Show Bool where
    show = ...

instance Show String where
    show = ...

이처럼 show라는 함수는 실제로는 각 타입별로 구현이 다르지만, 우리는 타입별로 구분할 필요 없이 show라는 하나의 심볼만 사용하면 하스켈이 알아서 인자 타입에 맞는 실제show 함수 구현을 찾아서 불러주는 것입니다.

앞서 설명한 것처럼 이렇게 하나의 심볼을 타입에 따라 서로 다른 의미로 사용하는 방법을오버로딩이라고 부르고, 하스켈의 타입 클래스는 오버로딩을 구현하기 위한 방법입니다.

이해를 돕기 위해 직접 타입 클래스를 하나 만들어 보겠습니다. 아래에 정의한 Mergeable타입 클래스는 두 개의 인자를 받아서 하나로 합치는 함수 merge를 정의하고 있습니다.

class Mergeable a where
  merge :: a -> a -> a

ghcimerge 함수의 타입을 확인해보면 아래와 같이 Mergeable a라는 컨텍스트가 함수 타입 왼쪽에 표시되는 것을 볼 수 있습니다.

> :t merge
merge :: Mergeable a => a -> a -> a

이제 merge 함수를 Int 타입과 List 타입에 대해 정의해 보겠습니다.

instance Mergeable Int where
  merge x y = x + y

instance Mergeable [a] where
  merge x y = x ++ y

Int 타입의 merge 함수는 두 인자의 합을 계산하도록 했고, List 타입의 merge 함수는 두 인자를 병합하도록 하였습니다. ghci를 이용해 테스트해보면 아래와 같이 인자 타입에 따라 서로 다른 merge 함수가 호출되는 것을 확인할 수 있습니다.

> merge (1 :: Int) (2 :: Int)
3
> merge [1,2,3] [4,5,6]
[1,2,3,4,5,6]

추가로 Bool 같이 우리가 인스턴스를 정의하지 않은 타입에 대해서는 에러가 나는 것도 볼 수 있습니다.

> merge False True

<interactive>:23:1:
    No instance for (Mergeable Bool) arising from a use of ‘merge’
    In the expression: merge False True
    In an equation for ‘it’: it = merge False True

하스켈의 PreludeShow 외에도 Eq, Ord, Read, Enum, Bounded, Num, Integral,Floating 등 다양한 타입 클래스를 정의하고 있습니다. 이에 대한 설명은 다음 글에서 이어가도록 하겠습니다.

왜 하스켈을 배워야 하는가?

하스켈은 배우기 어려운 언어로 정평이 나있습니다. 순수(pure) 함수지연 연산(lazy evaluation), 하스켈 타입 시스템(System F와 type class 등등), 병렬 처리 등은 그저 시작에 불과합니다.

하스켈을 제대로 쓰기 위해서는 카테고리 이론에서 빌려온 Functor, Applicative Functor, Monad 같은 개념들을 익혀야 합니다. IO뿐만 아니라 대부분의 하스켈 코드가 이런 개념들을 활용해서 작성되어 있기 때문에 하스켈을 제대로 쓰려면 중요 개념들을 제대로 이해하고 있어야 합니다.

Monad 몰라도 IO 코드는 충분히 작성할 수 있고 MaybeEither, List 타입들을 사용할 수 있습니다만, Monad를 모를 거면 굳이 왜 하스켈을 배워야 할까요? 이미 Clojure, F#, Scala 등 대중적인 함수 언어들이 나와 있고, Java 8이나 C#, Swift, ES6에도 함수 언어의 요소가 상당히 반영되어 있는 상황에서 굳이 더 어렵기만 한 하스켈을 배워야 할 이유가 없어 보입니다.

우리가 사용하는 대중적인 프로그래밍 언어들은 어차피 모두 튜링 컴플리트(Turing complete)하기 때문에 계산가능성(computability)에 있어서는 차이가 없습니다. 한 언어가 계산할 수 있는 것을 다른 언어가 계산하지 못하지 않는다는 뜻입니다. 하스켈이 강력하다고 하지만 본질적으로 C++이나 Java가 할 수 없는 일을 할 수 있지는 않습니다.

왜 하스켈을 배워야 하는지 설명하기에 앞서 프로그래밍의 본질이 무엇인지 질문을 던져볼 필요가 있습니다. 프로그래밍은 기본적으로 컴퓨터에 일을 시키는 것입니다. “메모리 x에 있는 값을 레지스터에 로드해서 1을 더하고 다시 메모리 x에 저장해라”도 일종의 프로그램입니다.

하지만 이게 끝은 아닙니다. 우리가 푸는 문제는 메모리에서 값을 꺼내 1을 더한 다음 다시 메모리에 저장하는 수준이 아니라 현실 세계에서 발생하는 훨씬 더 크고 복잡한 문제이기 때문입니다. 그래서 프로그래밍의 본질은 한 번에 풀 수 없는 크고 복잡한 문제를 작은 문제들로 나누어서 해결하고 그렇게 나온 결과물들을 조합하여 다른 문제를 해결하는 것을 말합니다.

여기서 좋은 프로그램의 가장 중요한 특성으로 조합성(composability)이 등장합니다. 우리는 계속해서 크고 복잡한 문제를 풀어야 하고, 또한 비슷하지만 조금은 다른 문제들을 풀어야 합니다. 앞서 만들어 놓은 산출물을 쉽게 조합하여 새로운 문제를 해결할 수 있다면 프로그래머의 생산성은 비약적으로 늘 수 있기 때문입니다.

프로그래밍의 패러다임 변화는 조합성을 끌어올리려는 노력의 연속입니다. goto 문을 사용하던 어셈블리에서 서브루틴, 블록 구조, for/while 루프를 강조하는 구조적 프로그래밍(structured programming)이 나온 이유는 풀어야 하는 문제의 복잡도가 증가하여 프로그램을 조합할 더 좋은 방법이 필요했기 때문입니다.

구조적 프로그래밍이 많은 문제점을 해결했음에도 불구하고 80-90년대 우리가 풀어야 할 문제의 복잡도가 또 다시 가파르게 상승하면서 새롭게 등장한 패러다임이 객체지향 프로그래밍(object-oriented programming)입니다. 클래스나 객체의 개념, 캡슐화(encapsulation), 정보 은닉(information hiding) 등이 나온 이유도 더 복잡해진 문제를 풀기 위해 더 좋은 조합 방법이 필요했기 때문입니다.

21세기에 들어와 함수 언어가 주목 받고 있는 이유는 우리가 풀어야 할 문제가 또 다시 더욱 복잡해지면서 클래스나 객체가 제공했던 수준 이상의 조합성이 필요해졌기 때문입니다. 함수 언어의 중요한 특징인 고차 함수(higher order function), 다형 함수(polymorphic function) 등이 궁극적으로 해결하고자 하는 문제도 결국 조합의 문제입니다.

하스켈을 배워야 하는 이유는 Functor, Bifunctor, Profunctor, Applicative functor, Monoid, Monad, Arrow, Lense, F-algebra, Adjunction 같은 수학적 개념들이 또 다시 복잡해지고 있는 문제를 풀 수 있는 새로운 조합 방법의 단서를 제공하기 때문입니다. 그리고 하스켈은 이런 측면에서 다른 함수 언어와도 비교하기 힘들 정도로 발전해 있습니다.

물론 하스켈을 통해 배우지 않아도 우리는 이미 많은 개념들을 알고 있고 실제로 사용하고 있습니다. ES6의 Promise, C#의 널 전파 연산자(null propagation operator), Python의 리스트 컴프리헨션(list comprehension) 등은 전혀 다른 기능처럼 보이지만, 내부적으로 Monad라는 같은 구조를 가지고 있습니다. 하스켈을 공부하면 이렇게 서로 상이해 보이는 개념들에 존재하는 공통 구조를 쉽게 발견할 수 있습니다.

정리하면, 프로그래밍의 본질은 조합을 얼마나 쉽게 할 수 있느냐에 있습니다. 그리고 하스켈은 프로그램을 조합에 사용할 수 있는 새로운 개념과 도구들을 제공합니다. 하스켈을 배워야 하는 이유는 이런 도구들을 습득하여 앞으로 닥칠 더 크고 복잡한 문제를 해결하는 프로그램을 작성하는 능력을 배양하는 데에 있습니다.

마지막으로 하스켈을 배워야 하는 또 다른 이유는 그 자체로 재미있기 때문입니다. 어렵기 때문에 재미가 없는 것이 아니라 퍼즐을 푸는 것처럼 새로운 것을 알아가고 깨닫는 재미가 있습니다. 특히, 이미 오랜 세월 개발을 해서 매너리즘에 빠지신 분들은 하스켈을 통해서 다시 프로그래밍의 재미를 느껴보시길 권합니다.

권력 중독

파워(power)는 우리가 일상에서 자주 사용하는 단어입니다. 우리말로 힘, 권력, 권한, 능력 등으로 번역되는 파워는 다양한 맥락에서 다양한 뜻으로 사용됩니다. 슈퍼맨이나 스파이더맨 같은 슈퍼 영웅들은 슈퍼 파워를 가지고 세상을 구하고, 개발 팀장은 개발 팀장에게 주어진 권한으로 프로젝트를 수행합니다.

개발 팀장에게 부여된 권한의 유일한 목적은 주어진 프로젝트를 훌륭하게 완수하는 것에 있습니다. 여러 사람이 모여서 같이 일을 하다 보면, 갈등이 생기기 마련이고 대화와 설득에도 불구하고 의견 조율에 실패한다면 팀장은 본인에게 부여된 권한을 이용하여 의사결정을 내리게 됩니다. 물론 팀원들의 의견을 무시하고 매번 팀장의 권한만을 내세우면 팀원들의 반발을 사고 신뢰를 잃게 되므로 이런 부작용을 잘 알고 불가피한 경우에만 선택적으로 사용해야 합니다.

하지만 반지의 제왕에서 절대반지를 얻은 골룸의 예처럼, 일단 작은 권력이라도 맛을 보게 되면 사람이 달라집니다. 팀장이 되면 주변 사람들의 반응이 달라집니다. 팀원 시절과 똑같은 의견을 이야기해도 팀원들이 좀 더 귀담아 듣고 지지해 주는 경우가 많습니다. “내가 팀장이니깐 시키는 대로 해”라는 말을 할 필요도 없습니다. 권위에 약하고 남들과 다른 의견을 제시하는 것을 껄끄러워 하는 우리 조직 문화에서는 알아서 팀장 말을 따라주기 때문입니다. 당연히 팀장은 점점 자기중심적이 됩니다.

일단 권력에 중독되고 나면 생기는 가장 큰 문제는 현실을 있는 그대로 바라보는 현실 감각을 잃는 데 있습니다. 프로젝트에서 발생하는 여러 문제들에 대해 객관적인 진단을 내리고 해결책을 찾는 대신, 본인 상상 속에서 상황을 진단하고 해결하려고 합니다. 물론 일부 팀원들이 문제를 제기하지만 듣기 싫어합니다. 이미 듣기 좋은 말을 듣는 데 익숙해졌기 때문입니다.

또한 일단 팀장이 권력에 의존하기 시작하면 모든 문제를 권력의 문제로 바라보게 됩니다. 팀원들이 지시를 잘 따르지 않으면 이를 본인 권력에 도전하는 것으로 여깁니다. 또한 모든 문제의 원인이 본인에게 주어진 권한이 부족한 탓으로 생각하고 더 많은 권한을 얻기 위해 위만 쳐다보게 됩니다. 이런 팀장은 더 이상 프로젝트 성공이 목적이 아니라 더 많은 권력을 얻는 것이 목적이 됩니다. 이른바 권력 중독입니다.

누구든 권력의 유혹에서 자유로울 수는 없지만, 특별히 더 권력지향적인 사람들이 있습니다. 이런 사람들은 두 가지 모습을 동시에 보입니다. 자신보다 지위가 높은 사람에게는 철저하게 순종하고, 지위가 낮은 사람들을 지배하려고 합니다. 본인의 무능함을 지적 당하면, 자신은 실력이 있는 사람인데 주어진 권한이 부족한 탓이라고 변명합니다. 개발 팀장이 이런 모습을 보이기 시작하면 더 이상 개발 팀장이 아니라 정치 팀장이 됩니다.

일반적으로 권력은 강함의 표현이라고 생각하지만, 인간 심리를 보면 사실 정반대입니다. 권력에 대한 집착은 강함이 아니라 약함에 있습니다. 홀로 세상을 마주할 강인한 자아가 없기 때문에 외부에 의존할 대상을 찾는 것인데 이런 욕구는 권력에 대한 집착으로 나타납니다. 개발 팀장 버전으로 바꿔서 풀어보면, 개발 실력과 프로젝트 관리 능력으로 팀을 이끌 자신이 없기 때문에 권력에 대한 집착이 나옵니다.

개발 팀장은 실력으로 승부하는 자리입니다. 팀원들은 지배해야 할 부하가 아니라 프로젝트를 같이 만들어 나가는 동료들입니다. 절대반지를 끼면 모습이 보이지 않게 되므로 그 힘으로 무엇을 하는지 알 수 없지만, 팀장이 휘두르는 권력은 모든 팀원들이 지켜보고 있습니다. 아주 작은 권력에 집착해 무엇보다 소중한 팀원들의 신뢰를 잃는 팀장이 되지 않기를 바랍니다.