함수 언어
하스켈은 함수 언어입니다. 함수 언어의 대표적인 특징은 함수를 다른 함수에 인자로 넘기거나 함수가 다른 함수를 리턴하는 일이 가능하다는 것입니다. 이런 함수를 고차 함수(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
또 다른 예로 fst
와 snd
함수가 있습니다. 둘 다 튜플을 인자로 받아 fst
는 튜플의 첫 번째 요소를 리턴하고, snd
는 튜플의 두 번째 요소를 리턴합니다.
> fst ('a', 1)
'a'
> snd ('a', 1)
1
함수의 인자 개수
하스켈 함수의 인자 개수는 항상 하나입니다. fst
와 snd
함수는 인자 'a'
와 1
두 개의 인자를 받는 함수가 아니라 ('a', 1)
이라는 튜플 값 하나를 인자로 받는 함수입니다.
하스켈에는 인자를 2개 이상 받는 함수가 존재하지 않습니다. 2개 이상의 인자가 필요하면 튜플을 넘기거나, 조금 있다 살펴볼 커리 함수(curried function)을 사용합니다.
함수의 타입
ghci
에서 :t
명령을 이용하면 주어진 값의 타입을 확인할 수 있습니다. 아래 예를 보면,'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
함수와 달리 구체적인 타입이 아닌 a
와 b
같은 소문자로 표시된 타입은 다른 타입으로 대체될 수 있기 때문에 Char
와 같은 타입과 구분하여 타입 변수(type variable)라고 합니다.
예를 들어, 튜플 ('x', 1::Int)
은 (Char, Int)
타입을 가지는데 이 튜플로 fst
함수를 호출하면 fst
함수의 인자 타입 a
는 Char
로, b
는 Int
로 대체되어 fst
의 타입은(Char, Int) -> Char
가 됩니다. 같은 방식으로 (False, 'y')
튜플을 호출하면 a
는False
의 타입인 Bool
, b
는 'y'
의 타입인 Char
가 되어 fst
의 타입은(Bool, Char) -> Bool
이 됩니다. 이처럼 하나의 함수가 인자에 따라 여러 타입을 가질 수 있는데, 이런 함수를 다형 함수(polymorphic function)라고 합니다.
연산자
하스켈은 +
, -
, &&
, ||
와 같이 특수 문자로 된 함수도 제공합니다. 이런 함수들을 연산자(operator)라고 부르는데, 연산자와 일반 함수의 차이점은 연산자는 infix로 일반 함수는 prefix로 사용한다는 점입니다. 즉, + 1 2
가 아니라 1 + 2
와 같은 형태로 함수를 호출하게 됩니다.
하지만 +
연산자를 prefix로 호출할 수도 있습니다. (+)
와 같이 연산자를 괄호로 싸주면 일반 함수와 마찬가지로 prefix로 호출이 가능합니다.
커리 함수(curried function)
앞서 하스켈 함수는 항상 인자를 하나만 받는다고 이야기하였습니다. 2개 이상의 인자가 필요할 경우, 튜플을 넘길 수 있다는 사실도 배웠습니다. 하지만 논리 연산자 (&&
)의 예를 보면 인자를 2개 받는 것처럼 보입니다.
분명 하스켈 함수는 인자를 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개의 인자 False
와 True
를 넘긴 것이 아니라, 먼저False
를 호출하고 그 결과로 함수가 리턴되면 다시 True
를 인자로 리턴된 함수를 호출한 것입니다. 이 과정을 좀 더 명시적으로 보기 위해 괄호를 추가하면 다음과 같습니다.
> ((&&) False) True
False
이렇게 함수가 함수를 리턴하는 방식으로 여러 개의 인자를 받을 수 있게 만든 함수를 커리 함수(curried function)라고 부릅니다. 이와 달리 여러 값을 하나의 튜플로 묶어 넘기는 것을 언커리(uncurried function) 함수라고 부릅니다. (&&)
는 커리 함수의 예이고, 앞서 살펴본 fst
와 snd
함수는 언커리 함수의 예입니다.
섹션
(+)
, (-)
와 같은 연산자들은 모두 커리 함수이기 때문에 인자를 하나만 줘서 호출하면 함수를 리턴합니다. 예를 들어, (+) 1
은 주어진 인자에 1을 더해서 리턴하는 함수가 됩니다.
이렇게 바이너리 연산자에 인자 하나만 줘서 새로운 함수를 만드는 경우가 흔하기 때문에 하스켈은 (+1)
과 같은 특별한 문법을 제공합니다. 이렇게 연산자에 인자 하나만 줘서 새로운 함수를 만드는 방법을 섹션(section)이라고 부릅니다.
피연산자의 위치에 따라 섹션을 만드는 방법은 2가지가 있습니다. 예를 들어 (1-)
는 1에서 주어진 값을 빼는 함수이고, (-1)
는 주어진 값에서 1을 빼는 함수입니다.