Conditional Expressions
다른 언어들과 마찬가지로 Haskell도 조건식이 존재합니다. 다만 statement가 아닌 expression을 이용합니다.
abs :: Int -> Int
abs n = if n >= 0 then n else -n
위의 예는 절대값을 만드는 간단한 function입니다. 아래와 같이 nested로 표현도 가능합니다.
signum :: Int -> Int
signum n = if n < 0 then -1 else
if n == 0 then 0 else 1
(Haskell에서는 애매한(ambiguous) 경우를 제거하기 위해 else가 필수입니다.)
Guarded Equations
위와 같은 조건식을 사용하는 것과 달리 guarded equation
이라는 것을 이용하여 표현이 가능합니다.
abs n | n >= 0 = n
| otherwise = -n
위의 예는 앞서 본 abs
와 같은 결과를 가져옵니다. 바로 비교가 필요한 function의 경우 guarded equation을 이용하여 표현이 가능합니다. signum
의 경우도 아래와 같이 더 이해하기 쉽게 표현할 수 있습니다.
signum n | n < 0 = -1
| n == 0 = 0
| otherwise = 1
(otherwise의 경우 else와 마찬가지로 꼭 포함이 되어있어야합니다. otherwise = True
라고 표현해야만 하죠.)
Pattern Matching
현재 대부분 언어들이 pattern matching을 지원합니다. (Scala의 경우 case
, C#의 경우 switch
가 한 예입니다.) Haskell에서는 다른 언어들과 달리 바로 적용이 가능합니다.
not :: Bool -> Bool
not False = True
not True = False
위의 예는 not
을 pattern matching 통해 구현한 것입니다. 아래도 같은 예입니다.
(&&) :: Bool -> Bool -> Bool
True && True = True
True && False = False
False && True = False
False && False = False
위와 같은 경우 아래와 같이 줄여서 표현도 가능합니다.
True && True = True
_ && _ = False
모두 True의 경우만 True이기 때문에 나머지 경우는 모두 False로 처리하면 됩니다. ( _(underscore)의 경우 어떠한 variable이라는 표현입니다.) 위의 경우도 좋지만 아래와 같이 표현하는 것이 더 좋습니다.
True && b = b
False && _ = False
True의 경우 b까지 체크하여 b를 리턴하지만, False의 경우 무엇을 넣어도 False 이므로 False를 만날 경우 뒤까지 연산하지 않고 False를 리턴합니다. 아래는 pattern matching의 경우 주의해야 하는 사항입니다.
- 모든 pattern matching은 위에서 아래로 해석됩니다.
- pattern matching에 이용된 variable은 모두 unique해야 합니다. (ex. b && b = b X)
List Patterns
Haskell의 모든 list는 내부적으로 (:) operator를 중복 이용하여 list를 만듭니다. [1,2,3,4]
의 경우 1:(2:(3:4:[])))
의 동작이 내부에서 발생하게 되는 것이죠. 따라서 내부에서 patten matching이 발생하는 것이죠. 여기서 말하는 pattern 은 (:) operator를 이용한 표현이라고 생각됩니다. (저는 솔직히 이게 이해가 잘 안갑니다. 하지만 다음 예를 보면 이해하기 쉬울 겁니다.)
(x:xs) pattern을 이용하여 List에 사용되는 function들을 보면서 추가적인 설명을 진행하겠습니다.
head :: [a] -> a
head (x:_) = x
tail :: [a] -> [a]
tail (_:xs) = xs
head (x:_)
의 경우 앞의 x만 신경쓰면 되고 tail (_:xs)
의 경우는 뒤의 값만을 신경쓰게 됩니다. (여기서 '신경'이라는 부분이 Complier가 해석하는 부분이하고 생각하시면 될 듯 합니다.)
(x:xs) pattern에 대해 추가적인 주의 사항입니다.
- (x:xs)는 non-empty list에 대해 적용이 가능합니다. 따라서
head []
는 에러를 발생합니다. - 또한 (x:xs) pattern은 사용시 괄호(parenthesis)와 함께 사용되야 합니다. 이유는 앞에서 다뤘듯이 function의 우선순위가 높기 때문입니다. 따라서
head x:_ = x
의 경우head x
가x:_
보다 먼저 적용되게 됩니다.
Lambda Expressions
Java를 포함한 최근 사용되는 많은 언어들이 lambda를 지원합니다. 간단히 lambda expression은 이름이 없이 구현된 function을 이야기합니다. 자세한 사항은 계속 보도록 하겠습니다. Haskell에서는 다음과 같이 표현합니다.
\x -> x + x
\
는 lambda를 의미하며 위의 경우 x를 두번 더하는 결과를 돌려줍니다. 사실 다른 언어에서의 lambda는 다소 closure에 가깝습니다. 이유는 다른 scope에 있는 변수들에 가깝기 때문입니다. 따라서 side-effect를 고려해야 합니다. (물론 그 둘의 차이를 느끼기엔 제가 내공이 부족합니다.)
그럼 lambda가 왜 유용할까요? 다음 예를 보면서 설명하겠습니다.
add x y = x + y
add = \x -> (\y -> x + y)
위는 전에 다뤘던 currying을 이용한 구현이고 아래는 lambda를 이용하였습니다. 아래의 경우가 다소 더 길지만 차근차근 읽기에는 더 좋습니다. currying과 같이 함수 명 뒤에 띄어쓰기(whitespace)로 매개변수를 표현하는 것보다는 훨씬 function의 형태를 보여줍니다.
const :: a -> b ->c
const x _ = x
const :: a -> (b -> c)
const x = \_ -> x
위의 경우(currying) 다소 내부적으로 const x
가 function을 돌려줄 것이라고 생각해야하지만, 아래 경우(lambda) const x
가 _ -> x
형태의 function을 돌려주는 것을 직관적으로 이해할 수 있습니다.
또한, lambda는 이름이 없는 function으로 번거로운 naming을 제거합니다.
odd n = map f [0..n-1]
where
f x = x*2 + 1
위와 같은 구현을 아래와 같이 작성할 수 있습니다.
odd n = map (\x -> x*2 + 1) [0..n-1]
lambda를 이용하여 where
로 표현하던 f
를 제거할 수 있었습니다.
Sections
Sections은 두개의 argument 사이에 사용되는 operator를 curried function처럼 앞쪽(infix)에 작성하는 방식입니다. (괄호와 함께 말이죠.)
> 1+2
3
> (+) 1 2
3
이러한 표현은 operator와 argument를 합쳐 표현하는 것도 가능합니다.
> (1+) 2
3
> (+2) 1
3
따라서, 앞에 예와 같이 +
operator에 대해 (+)
, (x+)
, (+y)
와 같이 부분 적용(partially applied)하는 방법에 대해 section이라고 말합니다.
그럼 왜 section이 유용할까요?
다음과 같이 간단한 기능을 가진 function을 표현할 수 있기 때문입니다.
(1+) -- successor function
(1/) -- reciprocation function
(*2) -- doubling function
(/2) -- halving function
이 경우 lambda를 사용하지 않아도 되며, 불필요한 naming또한 피할 수 있습니다.