아마 이글을 보고 있는 모든 개발자들은 List와 같은 collection에 굉장히 친숙할 것입니다. 또한, 이런 collection을 다루는 while, for, foreach등 많은 API를 다뤄왔을 겁니다.
하지만, 수학자(mathematicians)들이 가장 좋아하는 collection type은 set
입니다. 이런 set
은 중복 element는 삭제되는 등 List와 다른 면이 있습니다.
Haskell에서 List
는 head
와 tail
등 하나하나 읽히는 특성을 이용할 수 있습니다. 실제 이러한 기능을 모방하여 Java 8에서는 List를 Stream이라고 부르기도 합니다.
Set Comprehensions
그럼 수학에서 set
을 정의(Comprehension)하는 예제를 보겠습니다.
위의 공식의 결과는 {1,4,9,16,25}입니다. 하지만 실제 Set
을 이용항 구현하기에는 쉽지 않습니다. (index를 이용한 순차적 접근이 불가하기 때문입니다.)
List Comprehensions
따라서, Haskell에서는 비슷한 정의(Comprehension)를 list를 통해 구현합니다.
[x^2 | x <- [1..5]]
결과는 위와 같은 [1,4,9,16,25]입니다. 하지만 list
이죠.
- 위 표현 중
x <- [1..5]
는 generator라고 읽습니다. 따라서 x를 어떻게 생성하는 지를 정의하죠. - 정의(Comprehension)에는 다수의 generator가 있을 수 있습니다. 아래는 한 예입니다. x list를 기준으로 y list가 inner loop으로 동작하는 것을 확인 할 수 있습니다.
> [(x,y) | x <- [1,2,3], y <- [4,5]]
[(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]
- 위와 달리 아래와 같이 스면 y list를 기준으로 x list가 inner loop으로 동작합니다.
> [(x,y) | y <- [4,5], x <- [1,2,3]]
[(1,4),(2,4),(3,4),(1,5),(2,5),(3,5)]
실제 이러한 List Comprehension은 다른 많은 언어에 적용되어 있습니다. Scala에서는 for comprehension, C#에서는 linq comprehension등으로 말이죠.
Dependant Generators
위의 여러 Generator를 혼용하는 예를 보면서 우리는 2중 for문과 같은 동작을 볼 수 있습니다. 따라서 뒤에 있는 generator는 앞에 있는 generator의 값을 이용할 수 있습니다.
[(x,y) | x <- [1..3], y <- [x..3]]
위의 결과는 [(1,1),(1,2),(1,3),(2,2),(2,3),(3,3)]
입니다. 따라서 x의 값이 변할 때마다 y가 불려오면서 그 범위가 조정되는 것을 알 수 있습니다.
우리는 이러한 dependant generator를 이용하여 기본 API인 concatenate 를 구현할 수 있습니다.
concat :: [[a]] -> [a]
concat xss = [x | xs <- xss, x <- xs]
> concat [[1,2,3],[4,5],[6]]
[1,2,3,4,5,6]
xss
는 list of list로 그 중 하나의 list를 구하고(xs
), 그 안에 하나의 element(x
)를 구해 전체 결과를 돌려줍니다. 따라서, list of list들의 모든 element가 모여져 있는 결과를 볼 수 있습니다.
Guards
guard는 일종의 filter와 같이 list comprehension시 적용하여 값을 제한 할 수 있습니다.
[x | x <- [1..10], even x]
위의 결과는 [2,4,6,8,10]
입니다. 따라서 주어진 x의 범위 중 짝수(even)의 값만 적용 된 것을 볼 수 있습니다.
아래 예는 guard를 이용하여 약수(factor) list를 구하는 function입니다.
factors :: Int -> [Int]
factors n = [x | x <- [1..n], n `mob` x == 0]
> factors 15
[1,3,5,15]
이 function을 이용하여 어떠한 수가 소수(prime)인지 아닌지 판별하는 function을 만들 수 있습니다.
prime :: Int -> Bool
prime n = factors n == [1,n]
> prime 15
False
> prime 7
True
위의 prime
정의를 보면 factors n
의 결과가 1과 자기 자신이 나오지 않을 경우 소수가 아니라는 것을 돌려주게 됩니다.
이를 다시 이용해서 이렇게도 할 수 있겠네요.
primes :: Int -> [Int]
primes n = [x | x <- [2..n], prime x]
> primes 40
[2,3,5,7,11,13,17,19,23,29,31,37]
primes
는 n 이하의 모든 소수를 구하게 됩니다.
Zip Function
zip
function은 아래와 같이 두개의 list를 하나의 list로 합치는, 정확히는 paring하는 기능을 합니다. (concatenate와는 다른 동작을 보입니다.)
zip :: [a] -> [b] -> [(a,b)]
아래 예를 통해 자세히 살펴보겠습니다.
> zip ['a','b','c'] [1,2,3,4]
[('a',1),('b',2),('c',3)]
위의 결과를 통해 zip
은 결합 중 한 쪽 list의 element가 더이상 없을 경우 결합을 하지 않게 됩니다.
다음은 zip
을 이용하여 pairs
function을 구현한 것 입니다. (zip
을 사용한 적절한 예인 듯 싶네요.)
pairs :: [a] -> [(a,a)]
pairs xs = zip xs (tail xs)
> pairs [1,2,3,4]
[(1,2),(2,3),(3,4)]
위의 예를 보면, tail xs
를 통해 한 칸 뒤의 list를 구하고 이를 zip
하여 결과적으로 순차적으로 다음 값과 연결된 list를 돌려줍니다.
이렇게 구한 pairs
를 이용하여 sorted
function 을 구현 할 수 있습니다. 이 function 은 argument로 받은 list가 정렬이 되어있는지 확인하는 기능을 합니다.
sorted :: Ord a => [a] -> Bool
sorted xs = and [x <= y | (x,y) <- pairs xs]
> sorted [1,2,3,4]
True
> sorted [1,3,2,4]
False
우리는 zip
을 이용하여 list 내 element의 위치(position)을 정의할 수 있습니다. 전에 말씀드렸듯이, list는 array와 다르게 index를 통해 접근할 수 없습니다. list는 head부터 tail순으로 순차적으로 접근해야 합니다. 하지만, element 의 position을 정할 수 있다면 편리한 건 사실입니다.
따라서, 아래와 같이 positions
를 구현해보겠습니다.
positions :: Eq a => a -> [a] -> [Int]
positions x xs =
[i | (x1, i) <- zip xs [0..n], x == x1]
where n = length xs - 1
> positions 0 [1,0,0,1,0,1,1,0]
[1,2,4,7]
위에서 xs
를 받아 이를 [0..n]과 zip
을 통해 pairing합니다. 따라서 모든 element가 위치(position)을 가지게 되는 거죠. 그 뒤 앞서 배웠던 guard를 이용하여 x1
을 추스리게 됩니다.
String Comprehensions
Haskell에서는 String은 간단히 characters의 list입니다. 따라서 "abc" :: String
은 ['a','b','c'] :: [Char]
와 동일하게 해석됩니다.
따라서, 아래와 같은 결과들을 볼 수 있습니다.
> lengtht "abcde"
5
> take 3 "abcde"
"abc"
> zip "abc" [1,2,3,4]
[('a',1),('b',2),('c',3)]
아래 list comprehension을 string에 적용하여 한 문자열(string)내 소문자(lower)를 돌려주게 하고 이 수를 돌려주게 합니다.
lowers :: String -> Int
lowers xs = length [x | x <- xs, isLower x]
> lowers "Haskell"
6