Haskell 의 많은 function들은 list를 인자로 받습니다.
> head [1,2,3,4,5]
1
> tail [1,2,3,4,5]
[2,3,4,5]
> [1,2,3,4,5] !! 2
3
> drop 2 [1,2,3,4,5]
[3,4,5]
> length [1,2,3,4,5]
5
> sum [1,2,3,4,5]
15
> product [1,2,3,4,5]
120
> [1,2,3] ++ [4,5]
[1,2,3,4,5]
> reverse [1,2,3,4,5]
[5,4,3,2,1]
위 예는 이해하는데 크게 어렵지 않습니다. 하지만 몇가지 체크해야하는 사항이 있습니다. 바로 length와 같은 부분들인데요, 이는 기존 명령형(imperative) 프로그래밍 언어들처럼 array를 대상으로 하는 것이 아닌 list이기 때문에, 상수(constant)시간이 아닌 linear 시간이 걸린다는 점입니다. 이는 drop에도 물론 적용되겠지요.
Function Application in Haskell
Haskell은 수학(mathematics)와 다른 언어의 function과는 다른 점이 있습니다. 일단 수학에서의 function을 보면 다음과 같습니다.
f(a ,b) + c d
위와 같이 수학(mathematics)에서는 function을 적용하기 위해서는 괄호를 이용하여 인자를 넘겨줍니다. 뒤의 c와 d 사이에 공간(space)이 있기 때문에 곱셈으로 계산됩니다.
f a b + c*d
Haskell에서는 function이 곰셈보다 자주 사용되기 때문에 위와 같이 function의 인자를 공간(space)를 통해 표현하고 곱셈을 *을 통해 명시합니다.
f a + b
또한, 위와 같은 경우 Haskell은 function이 가장 우선 순위가 높기 때문에 f(a + b)
가 아닌 (f a) + b
로 해석됩니다.
계속해서 문법적인 부분을 보겠습니다.
average ns = sum ns `div` length ns
위와 같이 (single back quote)를 이용하여 infix operator를 적용할 수 있습니다. 물론 이것은 기존 방식과 동일하게 div (sum ns) length ns
로 적용 가능합니다.
(div
와 /
의 차이는 굉장히 중요합니다.)
또한, 다음 예에서 보듯이 python과 같이 scope의 구분은 indent(white space)를 통해 표현합니다.
a = b + c
where
b = 1
c = 2
d = a * 2
이외에 F#, groovy 등 다양한 언어들이 Functional Programming 언어로 사용됩니다. 하지만, Haskell과 비슷하기 때문에 어렵지 않게 이해할 수 있습니다.
Types in Haskell
Haskell에는 많은 부분들이 다른 언어들과 유사합니다. 여기서는 문법적으로 Type에 관련하여 특이한 사항만 집고 넘어가겠습니다.
e :: t
위의 예는 expression인 e가 돌려주는 type이 t라는 표현입니다. Java에서는 흔히 t e
라고 표현한 것과 동일합니다. 물론 작성하지 않아도 compile시 자동으로 type을 유추하게 됩니다. 이런 동작은 type inference 라고 합니다. (중요하진 않아요. 하지만 직접 정의할 필요도 있습니다.)
Integer in Haskell Haskell 에서는 두가지 Integer 타입이 나옵니다. 하나는
Int
인데, 이 경우는 fixed-precision integer로써 고정된 길이의 메모리를 가집니다. 하지만,Integer
의 경우 arbitrary-precision integer로 가변 메모리를 가져 큰 수의 곱셈이 가능합니다.List vs Tuple Haskell에는 다른 언어의 배열과 같은 역할의 Type이 두가지 있습니다. 간단히 List는 같은 Type의 집합이고 Tuple은 Type과 무관한 집합입니다. 따라서 Hugs와 같은 프로그램에서 아래와 같은 결과를 확인 할 수 있습니다.
:type [False, True]
[False, True] :: [Bool]
:type (False, True)
(False, True) :: (Bool, Bool)
Function Types
Function는 이미 많은 언어에서 가장 많이 사용되는 부분입니다. Haskell에서는 이러한 Function에 대한 Type을 다음과 같이 정의합니다.
t1 type인 값이 t2 type인 값에 맵핑되는 function의 type을
t1 -> t2
으로 표현
다음 예를 보면 이해하기 더 쉽습니다.
not :: Bool -> Bool
isDigit :: Char -> Bool
위의 정의에서 t1을 domain, t2를 range라고 말하기도 합니다.
다른 언어와 마찬가지로 함수에 대한 argument나 결과에 대해 다양한 Type이 가능합니다. 따라서 tuple이나 list를 이용할 수 있습니다.
add :: (Int, Int) -> Int
add (x, y) = (x + y)
zeroto :: Int -> [Int]
zeroto n = [0..n]
Curried Functions
만약 가변(arbitrary) argument와 결괄르 받아야하는 경우라면 어떨까요? Haskell에서는 다음과 같이 add
를 정의하는 것이 더 흔한 방법입니다.
add' :: Int -> (Int, Int)
add' x y = x + y
add'
는 integer 인 x를 인자로 받아 add' x
를 돌려줍니다. 이런 후 y를 인자로 받아 x + y를 돌려주게 되는 것이죠. 따라서, 어떠한 값을 인자로 받으면 function을 돌려주는 것입니다. 기존 add
와는 다르게 argument에 대해 하나씩 하나씩 처리하게 됩니다. 이러한 것을 curried function 이라고 말합니다. 다음 예를 하나 더 보겠습니다.
mult :: Int -> (Int -> (Int -> Int))
mult x y z = x*y*z
그럼 이러한 curried function을 사용하는 이유는 뭘까요? 보통 함수처럼 한번에 argument list 또는 tuple을 받지 않는 이유는 뭘까요? 이유는 바로 function을 부분적으로 적용(partially apply)할 수 있기 때문입니다. 유연해진다(flexible)는 걸 의미하죠.
add' 1 :: Int -> Int
take 5 :: [Int] -> [Int]
drop 5 :: [Int] -> [Int]
위의 예를 보시면, add'
, take
, drop
은 각각 처음받은 argument를 통해 리턴받은 function에 대한 기능이 변경됩니다. add'
의 경우 다음에 받는 argument에 1을 더할 것이고 take
의 경우 다음에 받은 argument(list)에 대해 5개의 값만 돌려주게 되는 것이죠. 이는 다른 언어와 다르게 function에 확장성을 부여해줍니다.
번외로 curried function을 위해 너무 많은 괄호(parentheses)를 사용할 경우 다음과 같이 표현해도 됩니다.
Int -> Int -> Int -> Int
Int -> (Int -> (Int -> Int))
위와 아래가 같은 의미로 해석됩니다. 즉 ->
를 기준으로 오른쪽이 모두 묶인다고 생각하면 됩니다.
Polymorphic Functions
간단히 말해, 여러 type을 가진 variables를 포함하는 function을 의미합니다. 한 예로 length
를 말할 수 있습니다.
length :: [a] -> Int
a를 type으로 가지는 list에 대해 Int 값을 돌려주는 function인데, a의 경우 어떠한 type도 가능합니다. 다른 언어들은 generics 과 같은 것을 이용하여 외부(explicitly)에서 표현해야하지만, Haskell의 경우 내부(implicitly)에서 적용됩니다. 즉, 외부에 정의 할 필요가 없습니다. 또한 Type variables를 위해 a이외에도 b,c로 표현합니다. (Java, C#에서는 S, T, R을 이용하는 것과 같습니다.)
SDK에서 제공되는 함수들을 이를 이용하여 정의되어 있습니다.
fst :: (a,b) -> a
head :: [a] -> a
take :: Int -> [a] -> [a]
zip :: [a] -> [b] -> [(a,b)]
id :: a -> a
Overloaded Functions
많은 프로그래밍 언어들이 function에 대해 overload를 제공합니다. 간단히 overload란 parameter들의 type이 다름에 따라 동일 function name을 가진 function을 두개 이상 만드는 것을 의미 합니다. 하지만, Haskell에서는 반대로 polymorphic type variable에 대해 domain을 정하는 것(restrict)을 의미합니다. 예로 sum
을 들 수 있습니다.
sum :: Num a => [a] -> a
sum
은 모든 type 을 받을 수 없습니다. 따라서 위와 같이 Num
의 경우만 받겠다고 정의하는 것입니다. Object orientation 관점에서는 Num interface를 상속(implement)하는 (type a)List에 대해서 정의하는 것으로 볼 수도 있습니다. 또는 Java의 Generics를 생각해도 되겠죠.
Haskell에는 다양한 Number type이 존재합니다.
- Num - Numberic types
- Eq - Equality types
- Ord - Ordered types
(+) :: Num a => a -> a -> a
(==) :: Eq a => a -> a -> Bool
(<) :: Ord a => a -> a -> Bool