모듈

하스켈 모듈은 관련된 함수, 타입, 타입클래스들을 하나로 묶은것입니다. 하스켈 프로그램은 모듈들의 집합이고, 메인 모듈에서 다른 모듈들을 로딩하여 정의된 함수들을 사용하는 것입니다. 이렇게 코드를 여러개의 모듈로 분리하는 것은 여러가지 이점이 있습니다. 서로 의존성이 적은 코드(loosely coupled)들이 충분히 일반적인 모듈로 분리되면 다양한 프로그램에서 재사용할 수 있습니다. 또한 코드를 어떤 목적을 가진 부분들로 나누어서 관리할 수 있습니다.

하스켈의 표준 라이브러리는 각각 공통의 목적을 제공하는 함수와 타입들로 분리되어 있습니다. 여기에는 리스트를 다루는 모듈, 복잡한 숫자를 다루는 모듈, 동시성 프로그래밍을 위한 모듈 등이 있습니다. 지금까지 다루었던 모든 함수, 타입, 타입클래스들은 Prelude 모듈의 일부였고, 하스켈에 기본으로 imported되어 있습니다. 이번 챕터에서는 몇가지 유용한 모듈과 여기서 제공하는 함수들에 대해서 알아보겠습니다.

하스켈은 문법적으로 import <module name>로 모듈들을 가져옵니다. 이 구문은 반드시 어떤 함수가 선언되기 이전에 나와야 해서 파일의 최상당에 위치하는 것이 일반적입니다. 하나의 파일에서는 여러개의 모듈을 가져올 수 있고, 라인단위로 구분해서 import문을 추가하면 됩니다.

Data.List 모듈은 리스트를 동작시키는 유용한 함수들을 제공합니다.

import Data.List

numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

import Data.List를 했을때 Data.List가 노출한 모든 함수들은 스크립트내 어디서든지 사용이 가능해집니다. nub은 리스트에 중복을 제거하는 함수로 Data.List에 정의되어 있습니다. length . nub 합성함수는 \xs -> length (nub xs)와 동일합니다.

ghci> :m + Data.List

GHCI를 사용할때는 위와 같은 방법으로 Data.List 모듈내 모든 함수들을 가져올 수 있습니다.

ghci> :m + Data.List Data.Map Data.Set

GHCI에서 여러개의 모듈을 한번에 로딩할때는 위와 같이 작성하면 됩니다. 하지만 만약에 이미 모듈을 가지고있는 스크립트를 로딩했다면 다시 :m +를 사용할 필요가 없습니다.

import Data.List (nub, sort)

만약 모듈에서 두개의 함수만 필요하면, 위와 같이 필요한 함수만 가져올 수 있습니다.

import Data.List hiding (nub)

만약 모듈에서 특정 함수만 제외하고 가져오고 싶다면, 위와 같이 할 수 있습니다. 여러개의 모듈이 동일한 이름의 함수를 export하고 있거나 잘못된 함수들을 제거할때 종종 사용됩니다. 예를들어 이미 우리 코드에서 nub라는 함수를 가지고 있어서 Data.List에서 nub 함수만 제외한 모든 함수를 가져오고 싶을때 사용됩니다.

import qualified Data.Map

함수명이 충돌날때 해결하는 다른 방법으로 qualified import가 있습니다. Data.Map은 key, value 자료구조를 제공하는 모듈로 filternull 함수와 같이 Prelude와 같은 이름을 가진 함수들이 있습니다. 이때 위 예제와 같이 qualified를 사용하여 선언하면 Data.Mapfilter를 사용하기 위해서는 Data.Map.filter로 사용해야 합니다. 반면에 filter를 사용하면 기본으로 제공되는 filter가 사용됩니다.

import qualified Data.Map as M

위 예제와 같이 Data.Map을 치환하여 M.filter로 사용할 수도 있습니다.

아래 링크에서 표준 라이브러리에 정의된 모듈들과 소스코드를 확인할 수 있습니다. 하스켈에서 어떤 모듈들과 함수들을 표준 라이브러리로 제공하는지 살펴보고 각 모듈의 소스코드를 보는 것은 하스켈을 이해하는 큰 도움이 됩니다.

https://downloads.haskell.org/~ghc/latest/docs/html/libraries/

또한 Hoogle에서는 모듈, 함수, 타입 등을 검색할 수 있는 하스켈 검색엔진을 제공합니다.

Data.List

Data.List 모듈은 리스트 다루는 모든 함수를 제공합니다. 편의를 위해서 Data.List의 일부 함수(map, filter 등)들은 Prelude 모듈에 export되어 있습니다. 또한 PreludeData.List의 함수들은 이름 충돌이 발생하지 않습니다. 여기서는 Data.List에서 제공하는 함수들에 대해서 살펴보겠습니다.

intersperse

리스트의 구성요소 하나와 리스트를 입력받아서, 리스트의 구성요소 사이사이에 입력받은 구성요소를 넣은 리스트를 리턴합니다.

ghci> intersperse '.' "MONKEY"
"M.O.N.K.E.Y"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]

intercalate

중첩 리스트와 리스트를 입력받아서, 중첩 리스트의 리스트 사이사이에 입력된 리스트를 넣은 리스트를 리턴합니다.

ghci> intercalate " " ["hey","there","guys"]
"hey there guys"
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

transpose

중첩 리스트를 재배열합니다. 중첩 리스트를 행렬로 본다면 열을 행으로 행을 열로 바꾼 리스트를 반환합니다.

ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
[[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hey","there","guys"]
["htg","ehu","yey","rs","e"]

다항식 , , 를 더할때, 리스트로 [0,3,5,9], [10,0,0,9], [8,5,1,-1]와 같이 표현한다면 아래와 같이 계산할 수 있습니다.

ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]

위와같이 3개의 리스트를 transpose하면 지수를 리스트의 인덱스로 설정하여 재배열한 후, 더하면 원하는 결과를 얻을 수 있습니다.

foldl', foldl1'

각각 foldl, foldl1 함수에 비해 게으르지않고 엄격한 함수 입니다. 매우 큰 리스트를 게으르게 fold할때 스택 오버플로우 에러를 종종 볼 수 있습니다. 이런 에러를 발생시키는 주범은 fold를 할때 accumulator값이 실제로 갱신되지 않아서 입니다. 실제로는 accumulator가 실제 결과(thunk라고 불림)로 만들어질때 그 값을 계산하는데, 이것은 재귀호출 중간에 만들어지는 모든 accumulator에서 thunk들을 만들어 스택 오버플로우를 발생시킵니다. foldl'foldl1'는 게으른 함수가 아니라서 thunk를 스택에 쌓는대신 그때그때 중간 값을 계산합니다. 따라서 스택 오버플로우가 발생한다면 foldl', foldl1' 함수로 전환해 볼 수 있습니다.

concat

중첩 리스트를 하나의 리스트로 이어주는 함수입니다.

ghci> concat ["foo","bar","car"]
"foobarcar"
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]

이 함수는 1레벨만 중첩을 제거합니다. 따라서 두번 중첩된 리스트를 하나의 리스트로 만들고 싶다면 concat을 두번해야 합니다. 예를들어 [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]]를 하나의 리스트로 만들기 위해서는 concat을 두번 호출해야 합니다.

concatMap

맵핑 함수와 리스트를 받아서 입력 리스트를 맵핑하여 한개의 리스트로 합쳐주는 함수입니다.

ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]

and

boolean의 리스트의 모든 값이 참이면 True를 리턴하는 함수입니다.

ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False

or

boolean의 리스트의 값중 하나라도 참이 있으면 True를 리턴하는 함수입니다.

ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False

any, all

any는 조건문(predicate)과 리스트를 입력받아서 리스트의 값들중 하나라고 조건문에 참이면 True를 리턴하는 함수입니다.
all은 조건문(predicate)과 리스트를 입력받아서 모든 리스트의 값들이 조건문에 참이면 True를 리턴하는 함수입니다.
anyall은 리스트의 모든 구성요소를 조건문에 확인해야할때 andor 대신에 사용됩니다.

ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
False
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
True

iterate

함수와 초기값을 받아서 초기값을 함수에 적용한 결과값을 다시 함수에 적용하는 것을 계속해서 반복하여 무한 리스트를 리턴하는 함수입니다.

ghci> take 10 $ iterate (*2) 1 
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++ "haha") "haha"
["haha","hahahaha","hahahahahaha"]

splitAt

숫자와 리스트를 받아서 리스트를 입력받은 숫자 위치를 기준으로 분리한 후, 튜플을 리턴하는 함수입니다.

ghci> splitAt 3 "heyman"
("hey","man")
ghci> splitAt 100 "heyman"
("heyman","")
*ghci> splitAt (-3) "heyman"
("","heyman")
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"

takeWhile

조건문(predicate)과 리스트를 입력받아서 주어진 조건문이 참일때까지만 가져온 리스트를 리턴하는 함수입니다.

ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "This is a sentence"
"This"

자연수의 세제곱값이 10000보다 작은 값들의 합을 구하려면 어떻게 해야 할까요?
[1..]^3를 하고 filter를 쓰고 더하려고하면 무한 리스트이기 때문에 영원히 끝나지 않을 것입니다. 이 문제를 takeWhile을 사용하면 아래와 같이 구할 수 있습니다.

ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361

무한 리스트에 ^3을 적용한 값이 10000보다 크면 중지하고 모든 수를 더해서 쉽게 답을 얻을 수 있습니다.

dropWhile

takeWhile와 유사하게 조건문이 거짓일때까지의 값들을 제외한 리스트를 리턴하는 함수입니다.

ghci> dropWhile (/=' ') "This is a sentence"
" is a sentence"
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]

(Stock, year, month, date)로 구성된 튜플의 리스트를 받았을때, stock이 천달러를 초과하는 시점은 아래와 같이 구할 수 있습니다.

ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)

span

takeWhile과 유사하지만 리스트 쌍을 리턴하는 함수입니다. 동일한 조건(predicate)에 동일한 리스트를 입력으로 takeWhile을 호출한 결과가 첫번째 리스트가 되고 takeWhile에 의해서 제외된 값들이 두번째 리스트가 됩니다.

ghci> let (fw, rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ", the rest:" ++ rest
"First word: This, the rest: is a sentence"

break

span 함수와 유사하지만 break 함수는 조건문이 첫번째 참일때 중단합니다. break pspan (not . p)와 동일한 기능을 합니다.

ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])

위 예제와 같이 break 함수는 두번째 리스트의 첫번째 값이 조건문을 만족하는 첫번째 값입니다.

sort

리스트를 정렬하는 함수입니다. 리스트내 값들의 타입클래스를 Ord에 포함되어야 합니다. 순서를 판단할 수 없는 값은 정렬을 할수없습니다.

ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "This will be sorted soon"
"    Tbdeehiillnooorssstw"

group

리스트를 받아서 동일한 값이 인접해 있으면 하위 리스트로 묶어주는 함수 입니다.

ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]

만약 리스트를 그룹핑하기 전에 정렬하면 리스트에서 각 값들이 몇번 나왔는지 알 수 있습니다.

ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]

inits, tails

inittail를 재귀적으로 아무것도 없을때 까지 호출한 결과들의 리스트를 리턴합니다. 아래 화면을 보면 쉽게 이해할 수 있습니다.

ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]

fold 함수를 사용해서 하위 리스트의 리스트를 검색하는 것을 구현하면 아래와 같습니다.

search :: (Eq a) => [a] -> [a] -> Bool
search needle haystack = 
    let nlen = length needle
    in foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)

먼저 검색할 대상 리스트에 tails를 호출하여 tail의 리스트를 만들고, 각 tail가 찾고있는 것으로 시작하는지 확인합니다. 여기서 take nlen x == needle이 x가 needle로 시작하는지 확인하는 방법입니다.

isInfixOf

입력받은 첫번째 하위 리스트가 두번째 대상 리스트에 포함되는지 검색하는 함수입니다. 만약 대상 리스트내의 어디에든 하위 리스트가 포함되면 True를 리턴합니다.

ghci> "cat" `isInfixOf` "im a cat burglar"
True
ghci> "Cat" `isInfixOf` "im a cat burglar"
False
ghci> "cats" `isInfixOf` "im a cat burglar"
False

isPrefixOf, isSuffixOf

isPrefixOf는 하위 리스트가 대상 리스트에 시작인지 확인하는 함수입니다.
isSuffixOf는 하위 리스트가 대상 리스트에 마지막인지 확인하는 함수입니다.

ghci> "hey" `isPrefixOf` "hey there!"
True
ghci> "hey" `isPrefixOf` "oh hey there!"
False
ghci> "there!" `isSuffixOf` "oh hey there!"
True
ghci> "there!" `isSuffixOf` "oh hey there"
False

elem && notElem

리스트안에 입력받은 구성요소가 포함되는지 여부를 확인하는 함수입니다.

partition

조건문(predicate)과 리스트를 입력받아서 리스트의 쌍을 리턴합니다. 첫번째 리스트는 조건문에 만족하는 구성요소들의 리스트이고, 두번째 리스트는 나머지 구성요소들의 리스트입니다.

ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOBMORGAN","sidneyeddy")
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])

spanbreak와 어떻게 다른지 이해하는 것이 중요합니다.

ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOB","sidneyMORGANeddy")

spanbreak는 조건문에 만족하는 첫번째 구성요소까지 첫번째 리스트로 만들고, partition은 만족하는 모든 구성요소를 첫번째 리스트 만듭니다.

find

조건문(predicate)과 리스트를 입력받아서 조건을 만족하는 첫번째 구성요소가 Maybe값으로 랩핑된 구성요소로 리턴됩니다. 다음 챕터에서 대수적인 데이터 타입에 대해서 더 자세히 다룰 것입니다. 여기서는 알아야 할 것은 Maybe값은 어떤 값을 가지거나 아무것도 없을 수 있다는 점입니다. 마치 리스트가 비어있거나 어떤 구성요소를 가질 수 있는 것처럼 Maybe값은 아무 것도 없거나 하나의 구성요소를 가질 수 있습니다. Integer의 리스트의 타입이 [Int]인 것처럼 Interger를 가질수도 있는 타입을 Maybe Int 라고 합니다.

ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a

find의 리턴 타입은 Maybe a입니다. 따라서 find의 결과는 구성요소가 있거나 없을수도 있습니다.

주가가 1000$를 넘는 순간을 찾는 문제는 아래와 같이 풀 수도 있습니다.

head (dropWhile (\(val,y,m,d) -> val < 1000) stock)

여기서 head를 사용하는 것은 안전하지 않다는 점에 유의해야 합니다. 주가가 영원히 1000$를 넘지않는다면 어떻게 될까요? dropWhile은 빈리스트를 리턴하고 head가 없기때문에 런타임 에러가 발생합니다. 하지만 find를 사용하면 아래와 같이 재작성될 수 있습니다.

find (\(val,y,m,d) -> val > 1000) stock

이 경우는 주가가 영원히 1000$를 넘지않아도 Nothing을 리턴하기 때문에 안전합니다. 만약 유효한 값이 있다면 Just (1001.4,2008,9,4)와 같이 리턴할 것입니다.

elemIndex

elem과 동일한 기능을 하지만 boolean값을 리턴하지않습니다. 찾고있는 구성요소의 index를 리턴합니다. 만약 찾는 구성요소가 리스트에 없으면 Nothing을 리턴합니다.

ghci> :t elemIndex
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing

elemIndices

elemIndex와 동일한 기능을 하지만 찾고있는 구성요소가 여러개인 경우 index들의 리스트를 리턴합니다. 구성요소가 한개도 없는 경우는 Nothing과 유사하게 빈리스트를 반환합니다.

ghci> ' ' `elemIndices` "Where are the spaces?"
[5,9,13]

findIndex, findIndices

findIndexfind와 동일한 기능을 하지만 조건에 만족하는 첫번째 구성요소의 index를 리턴합니다.
findIndices는 조건에 만족하는 모든 구성요소의 index 리스트를 리턴합니다.

ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
[0,6,10,14]

zip3, zip4, zipWith3, zipWith4

zip3는 3개의 리스트를 받아서 3개의 튜플의 리스트로 묶는 함수입니다.
zip4는 4개의 리스트를 받아서 4개의 튜플의 리스트로 묶는 함수입니다.
zipWith3는 3개의 인자를 받는 함수와 3개의 리스트를 받아서 묶은 후, 각 튜플을 입력받은 함수에 적용한 결과의 리스트를 리턴합니다.
zipWith4는 4개의 인자를 받는 함수와 4개의 리스트를 받아서 묶은 후, 각 튜플을 입력받은 함수에 적용한 결과의 리스트를 리턴합니다.
이런 zipzipWith의 변종 함수는 7개까지 있습니다. 또한 무한개의 리스트를 묶을때도 매우 좋은 방법이 있지만, 여기서는 다루지 않겠습니다.

ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
[7,9,8]
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]

zip 계열의 함수들은 모두 입력받은 리스트중 가장 짧은 리스트를 기준으로 잘려서 동작합니다.

lines

문자열을 입력받아서 라인단위로 분리된 리스트를 리턴합니다. lines는 파일이나 외부입력을 다룰때 매우 유용한 함수입니다.

ghci> lines "first line\nsecond line\nthird line"
["first line","second line","third line"]

예제에서 \n은 unix의 개행문자입니다. 하스켈의 문자열이나 문자들에서 백슬래쉬(\)는 특별한 의미를 가집니다.

unlines

lines와 반대로 여러개의 문자열의 리스트를 '\n'를 사용하여 하나의 문자열로 합칩니다.

ghci> unlines ["first line", "second line", "third line"]
"first line\nsecond line\nthird line\n"

words, unwords

words는 하나의 라인을 문자열로 받아서 단어들의 리스트로 분리합니다.
unwords는 단어들의 리스트를 하나의 문자열로 합칩니다.

ghci> words "hey these are the words in this sentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> words "hey these           are    the words in this\nsentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> unwords ["hey","there","mate"]
"hey there mate"

nub

리스트를 받아서 중복된 값들을 제거하고 유일한 값들의 리스트를 리턴합니다.

ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Lots of words and stuff"
"Lots fwrdanu"

delete

한 개의 구성요소와 리스트를 받아서 리스트내에서 입력받은 구성요소와 처음으로 일치하는 구성요소만 삭제된 리스트를 리턴합니다.

ghci> delete 'h' "hey there ghang!"
"ey there ghang!"
ghci> delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere ghang!"
ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere gang!"

//

두개의 리스트를 입력받아서 왼쪽 리스트에서 오른쪽 리스트와 매칭되는 값만 삭제한 리스트를 리턴합니다.

ghci> [1..10] \\ [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Im a big baby" \\ "big"
"Im a  baby"

[1..10] // [2,5,9]delete 2 . delete 5 . delete 9 $ [1..10]과 동일한 동작을 합니다.

union

두개의 리스트를 받아서 두번째 리스트에서 첫번째 리스트와 중복되는 구성요소를 제거하고 첫번째 리스트와 합친 리스트를 리턴합니다. 즉, 두 리스트의 합집합을 구합니다.

ghci> "hey man" `union` "man what's up"
"hey manwt'sup"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]

intersect

두개의 리스트를 입력받아서 양쪽에 모두 존재하는 구성요소들의 리스트를 리턴합니다. 즉, 교집합을 구합니다.

ghci> [1..7] `intersect` [5..10]
[5,6,7]

insert

리스트에 삽입할 값과 정렬이 가능한 리스트를 받아서 대상 리스트에 넣을 값보다 작거나 같은 구성요소 다음에 삽입하는 함수입니다.

ghci> insert 4 [3,5,1,2,8,2]
[3,4,5,1,2,8,2]
ghci> insert 4 [1,3,4,4,1]
[1,3,4,4,4,1]

이 예제에서 435 사이의 숫자로 해당 위치에 넣은 것을 확인할 수 있습니다.

ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]

만약 이미 정렬된 리스트에 넣는다면 결과 리스트도 정렬된 리스트입니다.

genericLength, genericTake, genericDrop, genericSplitAt, genericIndex, genericReplicate

length, take, drop, splitAt, !!, replicate는 모두 입력 인자로 Int를 받거나, Int를 리턴하는 함수입니다. 함수에 따라서 Integral이나 Num 타입클래스(함수에 따라서)를 받는다면 좀 더 일반적이고 유용하게 사용될 수 있습니다. 이렇게 보다 일반적인 함수들을 genericXXX 형태 제공합니다. 예를들어 length의 타입은 length :: [a] -> Int입니다. 만약 let xs = [1..6] in sum xs / length xs로 숫자들의 리스트의 평균을 구한다면, /Int를 사용할 수 없기때문에 타입에러가 발생합니다. 반대로 genericLength의 타입은 genericLength :: (Num a) => [b] -> a 입니다. Num은 부동소수점처럼 동작할 수 있기때문에 let xs = [1..6] in sum xs / genericLength xs는 잘 동작합니다.

nubBy, deleteBy, unionBy, intersectBy, groupBy

nub, delete, union, intersect, group은 각각 좀 더 일반적인 함수로 nubBy, deleteBy, unionBy, intersectBy, groupBy 함수를 가지고 있습니다. genericXXX 함수들은 동등성 체크를 ==으로 하는반면에 xxxBy 함수들은 동등 함수를 받아서 비교한다는 점(예를들어 groupgroupBy (==)과 동일)에서 함수명 규칙을 다르게 적용하였습니다.

ghci> let values = [-4.3, -2.4, -1.2, 0.4, 2.3, 5.9, 10.5, 29.1, 5.3, -2.4, -14.5, 2.9, 2.3]
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

값이 0보다 큰것과 작은것을 기준으로 하위 리스트로 분류하는 함수입니다. 만약 여기서 group을 사용했다면 인접한 값을 함께 그룹핑할 것입니다. 하지만 여기서 우리가 얻고자 하는 것은 음수인지 아닌지에 따라서 분류하는 것입니다. 이 예제에서는 음수 섹션과 양수 섹션으로 명확히 구분된 것을 볼 수 있습니다. 동등 함수는 두개의 요소를 받아서 둘다 양수이거나 둘다 음수이면 True를 리턴합니다. Data.Function.onon 함수를 활용하면 동등함수를 좀 더 명확하게 작성할 수 있습니다.

on

on 함수는 두개의 인자를 입력받는 함수(f), 한개의 인자를 받는 함수(g)를 받아서 두개의 값을 받아서 각각에 g를 적용하고 두개의 결과를 f를 적용하는 함수를 리턴합니다. on 함수는 아래와 같이 정의될 수 있습니다.

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g = \x y -> f (g x) (g y)

따라서 (==) `on` (> 0)\x y -> (x > 0) == (y > 0)과 같은 동등함수를 리턴합니다. on은 아래 예제와 같이 By 함수와 함께 자주 사용됩니다.

ghci> groupBy ((==) `on` (> 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

sortBy, insertBy, maximumBy, minimumBy

sort, insert, maximum, minimum 함수도 좀더 일반적인 함수들을 가지고 있습니다. groupBy와 같은 함수들은 두개의 값이 동일한지를 결정하는 함수를 받습니다. sortBy, insertBy, maximumBy, minimumBy 함수는 한개의 값이 다른 값보다 큰지, 작은지, 같은지를 판단하는 함수를 입력받습니다. sortBy의 타입은 sortBy :: (a -> a -> Ordering) -> [a] -> [a] 입니다. OrderingLT, EQ, GT를 값으로 가집니다. sortsortBy compare와 같습니다. 왜냐하면 compareOrd 타입클래스인 두개의 값을 받아서 순서 관계를 리턴하기 때문입니다.

리스트들은 비교할 수는 있지만 사전식으로 비교가 됩니다. 만약 리스트의 리스트가 있을때 리스트의 내용이 아니라 내부 리스트의 길이에 따라서 정렬하려면 어떻게 해야할까요? 아래 예와 같이 sortBy를 사용해서 해결할 수 있습니다.

ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]

여기서 compare `on` length는 마치 영어 문장을 쓰는 것 처럼 자연스러운 것을 볼 수 있습니다. compare `on` length\x y -> length x `compare` length y와 동일합니다. By 함수에서 동등함수(equality function)를 받을때는 주로 (==)onsomething 형태로 쓰고, 비교함수(ordering function)를 받을때는 주로 compare `on` something 형태로 사용합니다.

Data.Char

문자를 다루는데 유용한 함수를 포함하고 있는 모듈입니다. 문자열에 대한 filter나 mapping도 Data.Char 모듈에 포함됩니다.
Data.Char에는 문자들에 대한 속성을 체크할 수 있는 여러가지 함수를 제공합니다.

isControl

문자가 control 문자인지 확인합니다.

isSpace

문자가 공백문자(space, tab, newline, etc)인지 확인합니다.

isLower

소문자인지 확인합니다.

isUpper

대문자인지 확인합니다.

isAlpha

문자가 알파벳인지 확인합니다.

isAlphaNum

문자가 알파벳이나 숫자인지 확인합니다.

isPrint

출력가능한 문자인지 확인합니다. 예를들어 control 문자는 출력이 불가능합니다.

isDigit

문자가 0-9의 10진수 숫자인지 확인합니다.

isOctDigit

문자가 0-7 8진수 숫자인지 확인합니다.

isHexDigit

문자가 0-F 16진수 숫자인지 확인합니다.

isLetter

문자가 알파벳인지 확인합니다.

isMark

문자가 유니코드 마크 문자인지 확인합니다. 앞글자와 결합하여 악센트가 있는 마디를 만드는 문자로서 예를들면 프랑스어가 있습니다.

isNumber

문자가 numeric인지 확인합니다.

isPunctuation

문자가 구두점인지 확인합니다.

isSymbol

문자가 수학기호나 화폐문자인지 확인합니다.

isSeparator

유니코드 공백과 구분 기호를 확인합니다.

isAscii

유니코드 문자셋의 처음 128개 문자인지 확인합니다.

isLatin1

유니코드 문자셋의 처음 256개 문자인지 확인합니다.

isAsciiUpper

아스키 대문자인지 확인합니다.

isAsciiLower

아스키 소문자인지 확인합니다.

지금까지 살펴본 함수들의 타입은 Char -> Bool입니다. 문자열같은 것을 필터링하거나 Data.Listall 함수와 함께 활용되기도 합니다.

ghci> all isAlphaNum "bobby283"
True
ghci> all isAlphaNum "eddy the fish!"
False

all은 조건문(predicate)와 리스트를 받아서 모든 값이 조건을 만족하면 True를 리턴합니다.

ghci> words "hey guys its me"
["hey","guys","its","me"]
ghci> groupBy ((==) `on` isSpace) "hey guys its me"
["hey"," ","guys"," ","its"," ","me"]

isSpace를 활용해서 words 함수를 흉내낸 예제입니다. 하지만 공백이 제거되지 않고 있습니다.

ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me"
["hey","guys","its","me"]

filter를 사용하여 공백문자를 제거하였습니다.

ghci> generalCategory ' '
Space
ghci> generalCategory 'A'
UppercaseLetter
ghci> generalCategory 'a'
LowercaseLetter
ghci> generalCategory '.'
OtherPunctuation
ghci> generalCategory '9'
DecimalNumber
ghci> map generalCategory " \t\nA9?|"
[Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]

GeneralCategoryOrdering과 같은 열거형(enumeration) 타입입니다. 이것은 문자를 몇가지 가능한 카테고리로 보여줍니다. 이 카테고리를 확인할 수 있는 함수로 generalCategory가 있습니다. 이 함수의 타입은 generalCategory :: Char -> GeneralCategory입니다. 이 함수는 총 31개의 카테고리를 가지고 잇습니다. GeneralCategoryEq 타입클래스에 속해있고, generalCategory c == Space와 같이 테스트할 수 있습니다.

toUpper

문자를 대문자로 바꾸어 줍니다. 공백이나 숫자등은 변경되지 않습니다.

toLower

문자를 소문자로 바꾸어 줍니다.

toTitle

문자를 title-case로 바꾸어 줍니다. 여기서 title-case를 대문자와 동일합니다.

digitToInt

문자를 Int로 바꾸어 줍니다. 이때 문자는 '0'..'9', 'a'..'f', 'A'..'F' 범위내에 있어야 합니다.

ghci> map digitToInt "34538"
[3,4,5,3,8]
ghci> map digitToInt "FF85AB"
[15,15,8,5,10,11]

intToDigit

digitToInt와 반대입니다. 0..15 범위의 Int를 받아서 소문자로 바꾸어 줍니다.

ghci> intToDigit 15
'f'
ghci> intToDigit 5
'5'

ord, ord

문자를 상응하는 아스키 코드 숫자로 바꾸어 주거나 그 반대로 바꾸어 줍니다.

ghci> ord 'a'
97
ghci> chr 97
'a'
ghci> map ord "abcdefgh"
[97,98,99,100,101,102,103,104]

두 문자의 ord 차이는 곧 유니코드 테이블에서 얼마나 떨어져있는지를 의미합니다.

encode :: Int -> String -> String
encode shift msg = 
    let ords = map ord msg
        shifted = map (+ shift) ords
    in  map chr shifted

Caesar cipher는 문자를 알파벳의 고정된 숫자만큼 이동시켜 메시지를 암호화하는 원시적인 방법입니다.
먼저, 문자열을 숫자들의 리스트로 바꾸고, 숫자들의 리스트를 다시 문자로 바꾸기전에 각 숫자에 shift한 양을 추가합니다.
만약 composition을 사용한다면 함수의 몸제는 map (chr . (+shift) . ord) msg와 같이 작성될 수 있습니다 실행하면 아래와 같은 결과를 볼 수 있습니다.

ghci> encode 3 "Heeeeey"
"Khhhhh|"
ghci> encode 4 "Heeeeey"
"Liiiii}"
ghci> encode 1 "abcd"
"bcde"
ghci> encode 5 "Marry Christmas! Ho ho ho!"
"Rfww~%Hmwnxyrfx&%Mt%mt%mt&"

메시지를 디코딩하는 것은 기본적으로 메시지를 처음 옮긴 장소의 수만큼 뒤로 이동하는 것입니다.

decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
ghci> encode 3 "Im a little teapot"
"Lp#d#olwwoh#whdsrw"
ghci> decode 3 "Lp#d#olwwoh#whdsrw"
"Im a little teapot"
ghci> decode 5 . encode 5 $ "This is a sentence"
"This is a sentence"

Data.Map

연관 리스트(또는 사전)는 순서가 중요하지않은 키값 쌍을 저장하는데 사용되는 리스트입니다. 예를들어 핸드폰 번호를 저장하기 위해서 사람의 이름을 키로하고, 전화번호를 값으로 하여 연관 리스트에 넣을 수 있습니다. 저장되는 순서는 중요하지 않고, 단지 올바른 사람에 해당하는 올바른 번호를 가져올 수 있으면 됩니다.

phoneBook =   
    [("betty","555-2938")  
    ,("bonnie","452-2928")  
    ,("patsy","493-2928")  
    ,("lucille","205-2928")  
    ,("wendy","939-8282")  
    ,("penny","853-2492")  
    ]

첫번째 값은 key이고 두번째값은 value 입니다.

findKey :: (Eq k) => k -> [(k,v)] -> v
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs

주어진 key에 해당하는 value를 찾아주는 함수입니다. 간단하게 key와 리스트를 받아서, 리스트에 key와 같은 것으로 filter합니다. 필터링한 리스트의 첫번째 key-value에서 value만 리턴합니다. 만약 찾는 key가 리스트에 없다면 filter한 리스트는 빈리스트가 되어 head를 가져오는 과정에서 런타임 에러가 발생합니다. 따라서 에러를 막기위해 Maybe를 사용합니다.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) = if key == k
                         then Just v
                         else findKey key xs

만약 key를 찾지못하면 Nothing을 리턴하고, 찾으면 key에 상응하는 값인 Just something을 리턴합니다. 이 예제는 종료조건 -> 리스트의 head, tail 분리 -> 재귀호출로 이어지는 fold 패턴입니다. 따라서 아래와 같이 fold를 사용하여 구현할 수 있습니다.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing

일반적으로 표준 리스트 재귀 패턴을 직접 사용하는 것보다는 fold를 사용하는 것이 가독성과 식별이 쉽습니다. foldr이 보이면 모든 사람은 fold를 한다는 것을 알지만, 재귀를 쓰면 코드를 이해하기 위해서 시간을 들여야 합니다.

ghci> findKey "penny" phoneBook
Just "853-2492"
ghci> findKey "betty" phoneBook
Just "555-2938"
ghci> findKey "wilma" phoneBook
Nothing

우리는 Data.List에서도 lookup 함수를 구현할 수 있습니다. key 해당하는 value를 얻고 싶으면 찾을때까지 리스트를 순회할 수 있습니다. Data.Map 모듈은 이것은 좀 더 빠르게 하는(내부적으로 트리를 가지고 있기때문에) 함수를 제공하고, 또한 많은 유틸리티 함수들을 제공합니다. 여기서부터는 연관리스트 대신에 맵의 동작을 살펴보겠습니다.

import qualified Data.Map as Map

Data.MapPreludeData.List와 충돌하는 함수를 가지고 있으므로 qualified import를 사용합니다.
Data.Map에 제공하는 함수들을 추리면 아래와 같습니다.

fromList

연관리스트를 받아서 동일한 연관성을 가지는 맵을 리턴합니다.

ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]

만약 연관리스트에 중복된 키가 있다면 제거됩니다. fromList의 타입은 아래와 같습니다.

Map.fromList :: (Ord k) => [(k, v)] -> Map.Map k v

타입을 보면 k,v 쌍의 리스트를 받아서 타입k의 키들을 타입v로 맵핑하는 맵을 리턴합니다. 주목할 점은 일반 리스트를 연관 리스트로 만들때, 키는 같지만(Eq 타입클래스) 여기서는 트리내에 배치되기 위해서 순서가 있어야합니다.

키값이 Ord 타입클래스가 아닐때를 제외하면, key-value 연관을 위해서 항상 Data.Map을 사용해야 합니다.

empty

비어있는 맵을 나타내는 것으로서 입력이 없고 단지 비어있는 맵을 리턴합니다.

ghci> Map.empty
fromList []

insert

key, value, map을 입력받아서 맵에 해당 key, value를 포함한 새로운 맵을 리턴합니다.

ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100  Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]

비어있는 맵과 insert를 사용하면 fromList를 직접 구현할 수 있습니다.

fromList' :: (Ord k) => [(k,v)] -> Map.Map k v
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty

foldr를 사용하여 비어있는 맵에서 오른쪽부터 접으면서 key-value 쌍을 accumulator에 넣습니다.

null

맵이 비어있는지 검사합니다.

ghci> Map.null Map.empty
True
ghci>d Map.null $ Map.fromList [(2,3),(5,5)]
False

size

맵의 크기를 알려줍니다.

ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5

singleton

key, value를 받아서 입력받은 key-value쌍 한개만 가진 맵을 리턴합니다.

ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]

lookup

Data.List``lookup처럼 맵에서 동작합니다. 찾는 것이 있으면 Just something, 없으면 Nothing을 리턴합니다.

member

key와 맵을 입력받아서 맵안에 key가 있는지를 알려줍니다.

ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False

map && filter

리스트의 map,filter와 동일한 기능을 합니다.

ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)]
ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')]

toList

fromList와 반대의 기능을 합니다.

ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3
[(4,3),(9,2)]

keys && elems

keys는 key의 리스트를 리턴하고 map fst . Map.toList와 동일합니다.
elems는 value의 리스트를 리턴하고 map snd . Map.toList와 동일합니다.

fromListWith

fromList와 유사하지만 중복된 키들을 버리지 않고, 함수에 적용하여 결정하는 함수입니다.

phoneBook =   
    [("betty","555-2938")  
    ,("betty","342-2492")  
    ,("bonnie","452-2928")  
    ,("patsy","493-2928")  
    ,("patsy","943-2929")  
    ,("patsy","827-9162")  
    ,("lucille","205-2928")  
    ,("wendy","939-8282")  
    ,("penny","853-2492")  
    ,("penny","555-2111")  
    ]
phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String  
phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "betty" $ phoneBookToMap phoneBook
"342-2492, 555-2938"

여기서 만약 fromList를 사용한다면, 키가 중복된 몇몇 값들을 사라질 것입니다. 위와 같이 fromListWith를 사용하여 customize할 수 있습니다.

phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]  
phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
["827-9162","943-2929","493-2928"]

만약 중복된 키가 있으면 해당하는 키의 값들을 묶어서 찾은 키에 해당하는 모든 값들을 하나의 리스트에 리턴합니다. 번호들을 묶기 위해서 ++를 사용할 수 있습니다.

ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]

또다른 예로 중복된 키가 발견되면 값들중 가장 큰 것만 남기는 함수를 만들 수 있습니다.

ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]

또는 중복된 키의 값들을 모두 더할 수도 있습니다.

insertWith

fromListfromListWith가 있는 것처럼 insert에는 insertWith가 잇습니다. 맵에 key-value쌍을 넣지만, 만약 맵에 키가 이미 존재한다면 무엇을 해야할지를 결정하는 함수를 제공합니다.

ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]

여기서는 Data.Map에 있는 몇개의 함수만 알아보았습니다.

Data.Set

Set은 내부적으로 트리로 구현되어 있기때문에 모든 구성요소들에는 중복이 없고, 순서가 있습니다. Set은 리스트와 동일한 기능을할때 더 빠르게 동작합니다. Set의 주요 동작은 멤버쉽을 확인하거나, 추가, 삭제, Set을 리스트로 변환 등을 할 수 있습니다.

import qualified Data.Set as Set

Data.SetPreludeData.List와 충돌하는 함수를 가지고 있으므로 qualified import를 사용합니다.

text1 = "I just had an anime dream. Anime... Reality... Are they so different?"  
text2 = "The old man left his garbage can out and now his trash is all over my lawn!"

위와같은 두개의 텍스트가 있을때, 양쪽에서 모두 사용되는 문자들을 찾아보겠습니다.

fromList

리스트를 받아서 Set으로 바꾸는 함수입니다.

ghci> let set1 = Set.fromList text1
ghci> let set2 = Set.fromList text2
ghci> set1
fromList " .?AIRadefhijlmnorstuy"
ghci> set2
fromList " !Tabcdefghilmnorstuvwy"

위 예제에서 볼 수 있듯이 Set안의 모든 아이템은 유일하고, 순서가 있습니다.

intersection

두개의 Set을 받아서 양쪽에서 동일하게 가지고 있는 구성요소들의 리스트를 리턴합니다. 즉, 교집합을 구합니다.

ghci> Set.intersection set1 set2
fromList " adefhilmnorstuy"

difference

두개의 Set을 받아서 첫번째 Set에는 있는데 두번째 Set에는 없는 구성요소들의 리스트를 리턴합니다.

ghci> Set.difference set1 set2
fromList ".?AIRj"
ghci> Set.difference set2 set1
fromList "!Tbcgvw"

union

두개의 Set을 받아서 양쪽에 있는 모든 유일한 문자들의 리스트를 리턴합니다.

ghci> Set.union set1 set2
fromList " !.?AIRTabcdefghijlmnorstuvwy"

null, size, member, empty, singleton, insert, delete

ghci> Set.null Set.empty
True
ghci> Set.null $ Set.fromList [3,4,5,5,4,3]
False
ghci> Set.size $ Set.fromList [3,4,5,3,4,5]
3
ghci> Set.singleton 9
fromList [9]
ghci> Set.insert 4 $ Set.fromList [9,3,8,1]
fromList [1,3,4,8,9]
ghci> Set.insert 8 $ Set.fromList [5..10]
fromList [5,6,7,8,9,10]
ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5]
fromList [3,5]

isSubsetOf

두개의 Set을 받아서 첫번째 Set이 두번째 Set의 subset인지 확인합니다.

ghci> Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5]
False
ghci> Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
False

map && filter

ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,5,7]
ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,4,5,6,7,8]

toList

Set은 가끔 리스트에서 중복된 값을 제거하기 위해서 사용됩니다. fromList로 리스트를 Set으로 바꾸고 toList로 다시 리스트로 만들면 중복 데이터가 제거됩니다.Data.List에는 이미 중복 제거를 위한 nub 함수이 있지만 거대한 리스트에서 중복을 제거할때는 'nub'을 사용하는 것보다 빠릅니다. 그러나 nub은 구성요소의 타입이 Eq이면 사용이 가능하지만, Set을 사용하는 방법은 Ord이어야 합니다.

ghci> let setNub xs = Set.toList $ Set.fromList xs
ghci> setNub "HEY WHATS CRACKALACKIN"
" ACEHIKLNRSTWY"
ghci> nub "HEY WHATS CRACKALACKIN"
"HEY WATSCRKLIN"

크기가 큰 리스트에서는 setNub은 일반적으로 nub보다 빠릅니다. 하지만 위 예제에서 볼 수 있듯이 nubsetNub과 다르게 리스트의 구성요소의 순서를 보존합니다.

모듈 만들기

하스켈도 다른언어와 마찬가지로 모드를 여러개의 파일로 분리할 수 있습니다. 프로그래밍을 할때 비슷한 기능을 하는 함수를 하나의 모듈로 묶어서 관리하는 것은 좋은 습관입니다. 이렇게하면 다른 모듈에서 import해서 함수를 재사용할 수 있습니다.

여기서는 기하학적 물체의 부피와 면적을 계산하는 몇가지 기능을 제공하는 모듈을 Geometry.hs 파일에 만들겟습니다. 그리고 모듈명은 Geometry로 하겠습니다. 먼저 제공할 함수들을 명시하고, 각 함수를 구현해보겠습니다.

module Geometry  
( sphereVolume  
, sphereArea  
, cubeVolume  
, cubeArea  
, cuboidArea  
, cuboidVolume  
) where  

sphereVolume :: Float -> Float  
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)  

sphereArea :: Float -> Float  
sphereArea radius = 4 * pi * (radius ^ 2)  

cubeVolume :: Float -> Float  
cubeVolume side = cuboidVolume side side side  

cubeArea :: Float -> Float  
cubeArea side = cuboidArea side side side  

cuboidVolume :: Float -> Float -> Float -> Float  
cuboidVolume a b c = rectangleArea a b * c  

cuboidArea :: Float -> Float -> Float -> Float  
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  

rectangleArea :: Float -> Float -> Float  
rectangleArea a b = a * b

꽤 표준적이 기하학이지만 유의할 점이 몇가지 있습니다. 여기서 큐브는 직육면체의 특별한 경우일 뿐이므로 모든 길이가 같은 직육면체로 처리하여 면적과 부피를 정의했습니다. 변의 길이에 따라서 사각형의 넓이를 계산하는 rectangleArea라는 헬퍼함수도 정의했습니다. cuboidAreacuboidVolume 함수에서 이 헬퍼함수를 사용했지만 export하지는 않았습니다. 왜냐하면 이 모듈에서는 3차원 객체를 다루기위한 함수를 제공하는 모듈이기 때문입니다.

import Geometry

모듈을 사용하기 위해서는 먼저 import 합니다. Geometry.hs 파일은 import하는 프로그램과 동일한 폴더에 있어야 합니다.

모듈들은 계층적인 구조로 만들수도 잇습니다. 각 모듈은 여러개의 서브모듈을 가질 수 있고 서브모듈들은 또 자신의 서브모듈들을 가질 수 있습니다. 여기서는 Geometry를 세개의 서브모듈을 가지 모듈로 쪼개보도록 하겠습니다.

우선 Geometry라는 폴더를 만들겠습니다. 여기서 폴더의 첫글자가 대문자라는 점을 명심하세요. 이 폴더에 Sphere.hs, Cuboid.hs, Cube.hs 파일을 만듭니다. 그리고 각 파일을 아래와 같이 작성합니다.

Sphere.hs

module Geometry.Sphere  
( volume  
, area  
) where  

volume :: Float -> Float  
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)  

area :: Float -> Float  
area radius = 4 * pi * (radius ^ 2)

Cuboid.hs

module Geometry.Cuboid  
( volume  
, area  
) where  

volume :: Float -> Float -> Float -> Float  
volume a b c = rectangleArea a b * c  

area :: Float -> Float -> Float -> Float  
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  

rectangleArea :: Float -> Float -> Float  
rectangleArea a b = a * b

Cube.hs

module Geometry.Cube  
( volume  
, area  
) where  

import qualified Geometry.Cuboid as Cuboid  

volume :: Float -> Float  
volume side = Cuboid.volume side side side  

area :: Float -> Float  
area side = Cuboid.area side side side

먼저 Geometry.Sphere는 Geometry 폴더내에 있고 모듈이름은 Geometry.Sphere 입니다. 다른 두 모듈에도 동일한 방식으로 처리했습니다. 또한 각 모듈에 동일한 이름의 함수가 있습니다. 각 함수는 다른 모듈에 있기때문에 동일한 이름으로 정의할 수 있습니다. 대신 특정 모듈의 함수를 가져다 쓸때는 qualified import를 사용해야 합니다.

import qualified Geometry.Sphere as Sphere  
import qualified Geometry.Cuboid as Cuboid  
import qualified Geometry.Cube as Cube

위와같이 qualified import를 한 후에 Sphere.area, Sphere.volume, Cuboid.area와 같이 호출해서 사용할 수 있습니다.

다음 번에는 실제로 많은 기능을 가진 파일을 작성할때 공통된 용도의 기능을 확인하고 별도의 모듈로 분리해보시기 바랍니다. 그리고 동일한 기능을 필요로하는 프로그램을 작성할때 모듈을 재사용할 수 있습니다.

results matching ""

    No results matching ""