INDIES

[Learn You a Haskell For Great Good!] 7. 모듈 본문

Haskell/LYAH

[Learn You a Haskell For Great Good!] 7. 모듈

jwvg0425 2015. 2. 10. 14:47



Learn You a Haskell For Great Good!


이 게시글은 http://learnyouahaskell.com/chapters 사이트에 올라와있는 글을 한글로 번역한 것입니다.의역이 굉장히 많으니 주의...


7. 모듈(Modules)


모듈 불러오기


 Haskell 모듈은 서로 연관 있는 함수와 타입, 타입 클래스의 집합이야. Haskell 프로그램은 메인 모듈(main module)이 다른 모듈들을 불러와서, 그 내부에 정의된 함수들을 이용해 뭔가 작업을 하는 모듈들의 집합이라고 볼 수 있지. 코드를 여러 개의 모듈에 나눠서 작성하는 건 꽤 장점이 많아. 모듈이 충분히 일반적이라면, 그 모듈의 함수들은 여러 프로그램에서 다양한 목적으로 사용될 수 있지. 코드를 서로 크게 의존하지 않는(이런 걸 느슨한 결합(loosely coupled)을 하고있다고 불러) 서로 독립적인 모듈에 나눠서 작성했다면, 이 모듈들은 나중에 재사용할 수 있어. 또 모듈은 코드를 어떤 용도를 가진 여러 개의 부분들로 나눔으로써 전체 작성 코드들을 더 관리하기 쉽게 만들어주지.

 Haskell 표준 라이브러리는 여러 개의 모듈로 나뉘어져 있고, 그 모듈들 각각은 서로 연관성 있는 함수들을 포함하고 있으며, 충분히 일반적인 용도로 쓰일 수 있어. 리스트를 다루는 모듈, 병렬 프로그래밍(concurrent programming)을 위한 모듈, 복소수를 다루는 모듈, 기타 등등. 지금까지 다뤄왔던 모든 함수들, 타입들, 그리고 타입클래스들은 기본적으로 포함되는 Prelude 모듈의 일부야. 이번 챕터에서, 표준 라이브러리의 유용한 몇 가지 모듈들과 그 내부의 함수들에 대해 알아볼거야. 하지만 그 전에 먼저, 어떻게 모듈을 프로그램에 포함시킬 수 있는 지 알아봐야겠지.

 Haskell 스크립트에 모듈을 포함시키는 구문은 import <모듈 이름> 이야. 이건 어떤 함수를 정의하는 것보다 반드시 먼저 와야하는 구문이고, 따라서 보통 포함 구문은 스크립트의 맨 위에 위치해. 당연한 이야기지만 한 스크립트가 여러 개의 모듈을 포함하는 것도 가능하지. 그냥 각각의 import 구문을 여러 줄에 걸쳐서 나열하면 돼. 한 번 Data.List 모듈을 포함시켜보자. Data.List 모듈은 리스트를 다루는 데 유용한 함수들을 굉장히 많이 갖고 있어. Data.List의 함수를 이용해 리스트가 중복되지 않는 원소를 몇 개 갖고 있는지 확인하는 함수를 한 번 만들어 보자.

  1. import Data.List  
  2.   
  3. numUniques :: (Eq a) => [a] -> Int  
  4. numUniques = length . nub  

 import Data.List를 수행했을 때, Data.List가 외부에 공개하는 모든 함수들이 전역 공간에서 사용 가능하게 돼. 무슨 뜻이냐면 Data.List의 함수들을 스크립트 어디에서나 사용 가능하게 된다는 거야. nub은 Data.List에 정의된 함수인데, 이건 List의 중복된 원소들을 모두 없애는 역할을 해. length 함수와 nub 함수를 . 연산자를 이용해 합친 length . nub은 \xs -> length (nub xs)와 동일한 의미의 함수를 만들어내지.

 GHCI를 쓸 때도 전역 공간에 모듈의 함수들을 불러 올 수 있어. GHCI에서 Data.List의 함수들을 쓰고 싶다면, 이렇게 하면 돼.

  1. ghci> :m + Data.List  

 GHCI에서 여러 개의 모듈을 불러오고 싶을 때, :m +를 여러 번 호출할 필요 없어. 한 번에 여러 개의 모듈을 불러올 수 있거든.

  1. ghci> :m + Data.List  

 하지만 불러온 스크립트가 이미 어떤 모듈을 포함하고 있다면, :m +를 쓰지 않아도 해당 모듈의 함수를 사용할 수 있어.

 만약 모듈에서 특정 몇 개의 함수만 필요하다면, 그 함수들만 선택적으로 포함시킬 수도 있어. Data.List에서 nub 함수와 sort함수만 포함시키고 싶다고 하자. 그럴 땐 이렇게 하면 돼.

  1. import Data.List (nub, sort)  

 또 모듈에서 일부 함수만 빼고 불러올 수도 있어. 이건 여러 모듈에 같은 이름의 함수가 정의되어있고, 그 중 필요한 것들만 빼고 나머지를 제거하고 싶을 때 종종 유용해. 스크립트에서 이미 nub이라는 함수를 정의한 상태라면, Data.List에서 nub 함수만 빼고 나머지 함수를 불러올 수도 있어. 이런 식으로 말야.

  1. import Data.List hiding (nub)  

 이름끼리 충돌이 일어날 때 취할 수 있는 또다른 방법은 명시적 포함(qualified import)이야. Data.Map 모듈은 키를 이용해 값을 찾는 자료구조를 제공해주는데, 이 모듈에는 filter나 null 같이 Prelude 모듈에 이미 포함된 함수와 동일한 이름의 함수를 다수 갖고 있어. 그래서 Data.Map 모듈을 포함시킨 다음에 filter 함수를 호출하면, Haskell은 어떤 함수를 써야할 지 알 수 없게 돼. 이런 문제를 해결하는 방법은 다음과 같아.

  1. import qualified Data.Map  

 이렇게 쓰면, Data.Map의 filter 함수를 쓰기 위해선 Data.Map.filter라고 써야 돼. 반면에 그냥 filter 라고 쓰는 건 우리가 잘 알고 있고 또 사랑하는 일반적인 filter 함수를 가리키게 되지. 하지만 Data.Map을 해당 모듈의 함수 앞에 항상 붙여 써줘야하는 건 귀찮은 일이야. 그래서 뭔가를 명시적 포함할 때는 그 이름을 더 짧게 다시 붙여줄 수 있어.

  1. import qualified Data.Map as M  

 이제, Data.Map의 filter 함수를 참조하기 위해, 간단히 M.filter 라고 쓰면 돼.

 표준 라이브러리에 어떤 모듈이 있는 지 알고 싶다면 이 유용한 레퍼런스를 이용해. Haskell 지식을 쌓는 좋은 방법은 그냥 표준 라이브러리 레퍼런스에 들어가서 모듈과 그 모듈의 함수가 뭐가 있는 지 슥 훑어보는거야. 또 여기서 각 모듈의 haskell 소스코드도 볼 수 있어. 모듈의 소스코드를 읽는 건 Haskell을 배우는 정말 좋은 방법이고, 네 Haskell 지식을 더 견고하게 만들어줄거야.

 어떤 함수나 그 함수가 어느 모듈에 위치해있는 지 검색하고 싶다면, Hoogle을 써. 이건 정말 멋진 Haskell 검색 엔진이야. 함수를 이름이나, 모듈 이름이나, 심지어는 타입 서명을 가지고도 검색할 수 있어.


Data.List


 Data.List 모듈은 말 그대로 리스트에 관한 모듈이야. 이건 리스트를 다루는 몇가지 굉장히 유용한 함수들을 제공해. Prelude 모듈이 편의성을 위해서 Data.List 모듈의 함수 일부를 포함하고 있기 때문에, 우린 이미 Data.List의 함수들 몇 가지(map이나 filter 같은 것들)를 접해본 적 있어. Prelude 모듈이 Data.List에서 이미 포함하고 있는 몇몇 함수들을 제외하면 두 모듈 간에 이름이 충돌하는 함수느 없기 때문에, Data.List 모듈을 명시적 포함할 필요는 없어. 이제 이전에 다뤄본 적 없는 몇 가지 함수들을 한 번 살펴보자.

 intersperse 함수는 원소와 리스트를 인자로 받아서, 그 원소를 리스트의 모든 원소쌍 사이에 끼워넣어. 여기 그 예시가 있어.

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

 intercalate는 리스트와 리스트의 리스트를 인자로 받아서, 리스트의 리스트 각 원소들 사이에 첫번째 인자로 받은 리스트를 끼워넣어서 전체를 하나의 리스트로 만들어버려.

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

 transpose 함수는 리스트의 리스트를 인자로 받아서 그걸 전치(transpose)시켜. 리스트의 리스트를 이차원 행렬로 보면, 열은 행이 되고 행은 열이 되는 거지.

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

 방정식 $3x^{2}+5x+9$와 $10x^{3}+9$, $8x^{3}+5x^{2}+x-1$이 있고, 이 둘을 서로 더하고 싶다고 하자. 이런 방정식을 Haskell에서 표현하기 위해 [0,3,5,9], [10,0,0,9], 그리고 [8,5,1,-1]이라는 리스트를 이용할 수 있어. 이제, 그걸 더하기 위해선 이렇게 하면 돼.

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

 이 세 개의 리스트를 전치시켰을 때, 세제곱 항들은 첫 번째 열에, 제곱 항들은 두 번째 열에, 하는 식으로 위치하게 돼. sum 함수를 그 리스트에 매핑시키면 원하는 결과를 얻을 수 있지.

 foldl'foldl1'은 게으른 foldl과 foldl1의 엄격한 버전이야. 엄청 큰 리스트에 대해 게으른 fold를 이용하면, 아마 스택 오버플로우 에러가 일어나게 될거 야. 이건 fold의 게으른 특성 때문에, fold가 일어날 때 누적값이 실제로 갱신되지 않아서 그래. 내부적으로 이런 누적값은 나중에 이 값이 뭐냐고 물었을 때 그 실제 값을 계산해주겠다는 약속(thunk라고 불러)을 만들어 내. 이건 모든 중간 과정에서의 누적값 계산에서 일어나는 일이고, 이 모든 thunk들이 스택 오버플로우를 일으키는 거지. 엄격한 fold는 게으르지 않고, 중간 과정에서 thunk로 스택을 채우는 대신 실제로 그 값이 뭔지 그 때 그 때 계산해. 따라서 게으른 fold를 쓸 때 스택 오버플로우 에러가 일어났다면, 그걸 엄격한 버전으로 바꿔봐.

 concat는 리스트의 리스트를 그냥 해당 원소의 리스트로 만들어줘.

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

 이건 중첩의 단계를 한 단계 낮추는 거야. 그러니까 [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]같은 리스트의 리스트의 리스트를 그냥 리스트로 만들고 싶다면, concat함수를 2번 써야 돼.

 concatMap은 먼저 list에 함수를 매핑하고, 그 다음에 해당 list에 concat를 적용시켜.

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

 and는 논리 값의 리스트를 인자로 받아서, 해당 리스트의 모든 인자가 True일 때만 True를 리턴해.

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

 or은 and랑 비슷한데, 리스트에 있는 값중 단 하나라도 True라면 True를 리턴해.

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

 anyall은 술어를 인자로 받아서 리스트의 모든, 혹은 어떤 인자가 해당 술어를 만족하는 지 검사해. 리스트에 함수를 매핑한 뒤 and, or 함수를 쓰기보다는 보통 그럴 때는 이 두 함수를 많이 써.

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

 iterate는 함수와 시작값을 인자로 받아. 그리고 시작값에 해당 함수를 적용시켜서 결과값을 만들고, 다시 그 결과값에 해당 함수를 적용시키고, 또 그 결과에 함수를 적용시키고... 이 과정을 반복해. 결과적으로 이 함수는 무한 리스트의 형태를 가진 결과를 리턴시켜.

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

 splitAt 함수는 숫자와 리스트를 인자로 받아. 그리고 이건 리스트를 가능한 개수만큼 잘라서, 두 개의 리스를 튜플로 만들어 리턴해.

  1. ghci> splitAt 3 "heyman"  
  2. ("hey","man")  
  3. ghci> splitAt 100 "heyman"  
  4. ("heyman","")  
  5. ghci> splitAt (-3"heyman"  
  6. ("","heyman")  
  7. ghci> let (a,b) = splitAt 3 "foobar" in b ++ a  
  8. "barfoo"  

 takeWhile 함수는 정말 유용한 함수야. 술어가 참인 동안 리스트에서 원소를 취하고, 술어를 만족하지 않는 원소랑 맞닥뜨리는 순간, 거기서 리스트르 잘라 내. 이건 굉장히 유용해.

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

 10,000보다 작은 세제곱 수의 합을 알고 싶다고 하자. (^3)을 [1..]에 매핑하고, 거기에 필터를 적용한다음에 더하거나 할 수는 없어. 왜냐하면 무한 리스트에 대한 필터링은 절대 끝나지 않거든. 넌 리스트의 원소가 모두 오름차순이라는 걸 알지만 Haskell은 그렇지 않잖아. 그래서 이렇게 써야 돼.

  무한 리스트에 (^3)을 적용하고, 한 번 10,000을 넘는 원소를 마주쳤을 때, 리스트는 그 시점에서 잘려. 이제 쉽게 그걸 합할 수 있어.

 dropWhile은 비슷한데, 이건 술어가 참인 모든 원소들을 버려. 한 번 술어가 False가 되면, 그 시점에서 리스트의 나머지 부분을 돌려줘. 이건 굉장히 유용하고 사랑스러운 함수야!

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

 매일의 주가를 나타낸 리스트가 있다고 하자. 이 리스트는 첫번째 원소는 주가, 두번째 원소는 년도, 세번째 원소는 달, 네번째 원소는 일인 튜플로 만들어져 있어. 그리고 처음으로 주가가 1000 달러를 넘긴 날짜를 알고 싶어!

  1. 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)]  
  2. ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)  
  3. (1001.4,2008,9,4)  

 span은 takeWhile하고 비슷하지만, 얘는 리스트의 페어를 리턴해. 첫 번째 리스트는 takeWhile이 똑같은 인자를 갖고 호출되었을 때와 동일한 결과값을 포함하게 되고, 두 번째 리스트는 takeWhile을 호출했을 때 버려질 부분(리스트의 나머지 부분)을 포함하게 돼.

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

 span이 리스트를 술어가 참일 때까지 이어지지만, break는 술어가 처음으로 참이 된 시점에서 멈춰. break p 라고 적는 건 span (not . p)라고 적는 것과 똑같아.

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

 break를 쓸 때, 결과에서 두 번째 리스트는 술어를 처음으로 만족하는 원소를 기점으로 시작해.

 sort는 단순히 리스트를 정렬해. 리스트에 속한 원소의 타입은 Ord 타입클래스에 속해 있어야 해. 리스트의 원소가 어떤 순서에 맞춰서 정렬될 수 없다면, 리스트를 정렬할 수 있을리가 없으니까.

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

 group은 리스트를 인자로 받아서 서로 같은 인접한 원소들을 리스트로 만들어.

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

 만약 리스트를 그룹핑하기 전에 정렬한다면, 각 원소가 리스트에 얼마나 많이 나타나는지 알아낼 수 있어.

  1. 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]  
  2. [(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]  

 initstails는 init과 tail하고 비슷해. 다만 이건 리스트에 아무것도 남지 않을 때까지 이 함수를 재귀적으로 적용해. 봐봐.

  1. ghci> inits "w00t"  
  2. ["","w","w0","w00","w00t"]  
  3. ghci> tails "w00t"  
  4. ["w00t","00t","0t","t",""]  
  5. ghci> let w = "w00t" in zip (inits w) (tails w)  
  6. [("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]  

 리스트의 부분 리스트를 검색하는 걸 구현하기 위해 fold를 이용할 수 있어.

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

 먼저 우리가 검색할 리스트에 tails 함수를 호출해. 그리고 각 tail에 대해 이게 우리가 찾는 리스트로 시작하는지 검사하는 거야.

 이런 식으로 동작하는 함수가 실제로 isInfixOf라는 함수로 구현되어 있어. isInfixOf 함수는 리스트에 찾고자 하는 부분 리스트가 있다면 True를 리턴해.

  1. ghci> "cat" `isInfixOf` "im a cat burglar"  
  2. True  
  3. ghci> "Cat" `isInfixOf` "im a cat burglar"  
  4. False  
  5. ghci> "cats" `isInfixOf` "im a cat burglar"  
  6. False  

isPrefixOfisSuffixOf는 각각 리스트의 시작과 끝에서 해당 부분 리스트를 검색해.

  1. ghci> "hey" `isPrefixOf` "hey there!"  
  2. True  
  3. ghci> "hey" `isPrefixOf` "oh hey there!"  
  4. False  
  5. ghci> "there!" `isSuffixOf` "oh hey there!"  
  6. True  
  7. ghci> "there!" `isSuffixOf` "oh hey there"  
  8. False  

 elemnotElem은 해당 원소가 리스트 안에 있는 지, 없는 지 검사해.

 partition은 리스트와 술어를 취해서, 리스트의 페어를 돌려줘. 결과에서 첫번째 리스트는 술어를 만족하는 모든 원소를 포함하고 있고, 두 번째 리스트는 그렇지 않은 원소들을 모두 포함하고 있어.

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

 이 함수가 span과 break와 어떻게 다른 지 이해하는 게 중요해.

 span과 break가 술어를 만족하거나 만족하지 않는 원소를 만나는 시점까지 딱 한 번만 동작하지만, partition은 리스트 전체를 검사해서 술어를 따르는 것과 그렇지 않은 것을 구분해.

 find 는 리스트와 술어를 취해서 해당 술어를 만족하는 첫번째 원소를 돌려줘. 하지만 이건 그 원소를 Maybe로 감싼 값을 돌려줘. 다음 챕터에서 대수적(algebraic) 데이터 타입을 감싸는 것에 대해 더 깊이 배울 거지만 일단 지금은 이렇게만 알아두면 돼. Maybe 값은 Just something 이거나 Nothing이 될 수 있다는 것. 리스트가 텅 빈 리스트거나 원소를 몇 개 가질 수 있는 것과 비슷하게, Maybe 값은 원소가 없거나 하나의 원소를 가질 수 있어. 그리고 정수의 리스트는 [Int]라고 쓰는 것과 유사하게, 정수를 갖는 maybe 타입은 Maybe Int야. 어쨌든, find 함수를 한 번 써 보자.

 find의 타입을 봐봐. 이 결과 값은 Maybe a야. 이건 원소를 여러 개, 혹은 안 가질 수 있는 리스트인 [a]라는 타입을 갖는 것과 비슷하게, 원소를 하나만 가질 수 있거나 하나도 못 가지는 Maybe 타입의 값을 나타내는 거야.

 주가가 처음으로 1000달러를 넘는 시점을 구하는 문제로 다시 돌아가보자. 이 때 head (dropWhile (\(val, y, m, d) -> val < 1000) stock)을 썼어. 이 때 head는 안전한 함수가 아냐. 만약 주가가 단 한 번도 1000을 넘지 못한다면 어떻게 될까? dropWhile을 수행한 결과는 텅 빈 리스트가 될거고 텅 빈 리스트의 head를 취하는 건 에러를 일으킬 거야. 하지만, 이걸 find(\(val, y, m, d) -> val > 1000) stock으로 바꿔써봐. 이게 더 안전해. 만약 주가가 단 한 번도 1000을 넘지 못한다면(그래서 술어를 만족하는 원소가 단 하나도 없다면) 그 결과로 Nothing을 받게 될 거야. 하지만 리스트에 정당한 답이 있다면, 아마 Just (1001.4, 2008, 9, 4)라는 값을 얻게 될 거야.

 elemIndex는 elem이랑 비슷해. 다만 이건 논리 값을 돌려주는 게 아니라, 우리가 찾는 원소의 인덱스 번호를 Maybe로 감싼 값을 돌려줘. 만약 리스트에 해당 원소가 없다면, Nothing을 돌려주지.

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

 elemIndices는 elemIndex랑 비슷해. 하지만 이건 인덱스들의 리스트를 돌려줘. 우리가 찾는 원소가 리스트에 여러 번 나타나면 그 위치가 다 리스트에 들어가게 되지. 인덱스 목록을 리스트를 이용해서 표현하기 때문에, Maybe 타입을 쓸 필요가 없어. 왜냐하면 하나도 찾지 못한 경우에는 텅 빈 리스트를 리턴하면 되고, 이건 Nothing을 리턴하는 것과 굉장히 비슷한 의미를 가져.

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

 findIndex는 find와 비슷해. 다만 이건 술어를 만족하는 첫번째 원소의 인덱스 값을 Maybe로 감싼 걸 돌려줘. findIndices는 술어를 만족하는 모든 원소들의 인덱스를 리스트의 형태로 리턴하고.

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

 앞에서 zip과 zipWith을 다룬 적이 있지. zip은 두 개의 리스트를 튜플로 묶거나 혹은 이진 함수(두 개의 인자를 취하는 함수)를 이용해서 하나로 합쳤어. 하지만 세 개의 리스트를 하나로 묶고 싶다면 어떻게 해야할까? 아니면 세 개의 리스트를 인자 세 개를 취하는 함수로 묶고 싶다면? 그럴 때를 위해서 zip3, zip4, ... 함수와 zipWith3, zipWith4, ... 함수가 있어. 이런 류의 함수가 7까지 존재해. 7까지 밖에 없는 게 좀 이상해 보일테지만, 이건 잘 동작해. 왜냐하면 8개 이상의 리스트를 합칠 일은 그렇게 많지 않거든. 그리고 무한 개수의 리스트를 합치는 정말 명석한 방법도 있지만, 그건 여기서 다루긴 아직 너무 어려운 이야기야.

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

 일반적인 zip처럼 이것도 리스트들 중에 길이가 가장 짧은 녀석의 길이에 맞춰서 리스트를 잘라내.

 lines는 파일이나 어딘가로부터의 입력을 다룰 때 유용한 함수야. 이건 문자열을 받아서 각각을 한 줄 단위로 자른 리스트를 돌려줘.

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

 '\n'은 유닉스에서 개행 문자를 의미해. 백슬래쉬는 Haskell 문자열과 문자에서 특별한 의미를 갖고 있어.

 unlines는 lines의 반대 역할을 하는 함수야. 이건 문자열의 리스트를 받아서 그걸 '\n'을 이용해 하나로 합쳐.

  1. ghci> unlines ["first line""second line""third line"]  
  2. "first line\nsecond line\nthird line\n"  

 wordsunwords는 한 줄의 문장을 단어 단위로 자르거나 단어들을 한 줄의 문장으로 합쳐줘. 이건 정말 유용해.

  1. ghci> words "hey these are the words in this sentence"  
  2. ["hey","these","are","the","words","in","this","sentence"]  
  3. ghci> words "hey these           are    the words in this\nsentence"  
  4. ["hey","these","are","the","words","in","this","sentence"]  
  5. ghci> unwords ["hey","there","mate"]  
  6. "hey there mate"  

 앞에서 nub 함수를 언급한 적이 있었지. 이건 리스트를 인자로 받아서 중복되는 원소를 없애고, 모든 원소가 유일한 원소인 리스트를 리턴해! 근데 이 함수는 이름이 좀 이상해. "nub"은 작은 혹(lumb)이나 어떤 것의 중요한 부분을 가리키는 말이라고 해. 내 생각에는, 얘네들이 함수 이름을 지을 때 옛날 사람들이 쓰는 말 대신에 실제로 쓰이는 말로 지었어야 했어.

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

 delete는 원소와 리스트를 취해서, 리스트에서 처음으로 나타나는 해당 원소를 삭제해.

  1. ghci> delete 'h' "hey there ghang!"  
  2. "ey there ghang!"  
  3. ghci> delete 'h' . delete 'h' $ "hey there ghang!"  
  4. "ey tere ghang!"  
  5. ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"  
  6. "ey tere gang!"  

\\은 리스트 뺄셈(list difference) 함수야. 이건 차집합하고 비슷하게 동작해. 오른쪽 편 리스트의 모든 원소에 대해, 왼쪽 리스트에서 나타나는 해당 원소들을 모두 삭제해.

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

[1..10] \\ ]2,5,9]는 delete 2 . delete 5. delete 9 $ [1..10]하고 비슷하게 동작해.

 union도 집합에서의 합집합과 비슷하게 동작하는 함수야. 이건 두 리스트를 합친 리스트를 돌려줘. 이 건 두 번째 리스트의 원소중 첫번째 리스트와 겹치지 않는 것들을 전부 첫번째 리스트에 추가한 리스트를 돌려주지. 다시 한번 말하지만, 중복되는 애들은 없어진다는 걸 주의해!

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

 intersect는 집합에서 교집합과 비슷하게 동작해. 이건 두 리스트 모두에서 나타나는 원소들로 이루어진 리스트를 돌려줘.

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

 insert는 원소와 정렬될 수 있는 원소들의 리스트를 인자로 받아서, 해당 원소를 여전히 그 다음 원소가 자신보다 크거나 같은 마지막 위치에 집어넣어. 다르게 말하자면, insert는 리스트의 시작지점에서부터, 자신보다 같거나 큰 원소를 찾을 때까지 진행한 다음, 해당 원소의 바로 앞 위치에 그 원소를 삽입해.

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

 첫번째 예에서 4는 3의 오른쪽이자 5의 바로 앞에 삽입됐고, 두번째 예에서는 3과 4 사이에 삽입됐어.

 insert를 정렬된 리스트에 사용하면, 그 결과 리스트 역시 정렬된 상태를 유지해.

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

 length, take, drop, splitAt, !! 그리고 replicate 함수는 왜 보통 인자로 Int를 취하거나 Int를 돌려줄까? 함수에 따라 Integral이나 Num 타입클래스에 속한 임의의 타입을 사용하는 게 더 일반적이고 사용하기 편할텐데 말이야. 거기엔 역사적인 이유가 있어. 하지만, 그걸 고치려면 이미 존재하는 코드 대다수를 파괴해야만 할 거야. 그래서 Data.List는 genericLength, genericTake, genericDrop, genericSplitAt, genericIndex, 그리고 genericReplicate라고 이름 붙은 좀 더 일반적인 동등한 함수들을 제공해. 예를 들어서, 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라고 쓰는 건 잘 동작해.

 nub, delete, union, intersect, 그리고 group 함수는 모두 그와 대응되는 일반적인 함수인 nubBy, deleteBy, unionBy, intersectBy, 그리고 groupBy 함수가 존재해. 이 둘 사이에 차이점은, 처음 언급한 함수들은 동등성 비교를 위해 ==을 이용하지만, By가 붙은 것들은 동등성을 비교하는 함수를 인자로 받아서 그걸 이용해서 둘의 동등성을 비교해. group 함수는 groupBy (==)과 동일해.

 예를 들어서, 매 초마다의 함숫값을 기록한 리스트가 있다고 하자. 이걸 값이 0이하일 때와 0보다 클 때에 따라 부분 리스트로 분할하고 싶어. 만약 일반적인 group 함수를 쓴다면, 이 건 그냥 인접한 값이 서로 같은 애들만 묶어버리지. 하지만 이걸 그 값이 0보다 큰 지 작은 지에 따라 그룹핑하고 싶다면, groupBy를 쓰면 돼! By 함수에 적용되는 함수는 동일한 타입의 두 개의 인자를 받아서 기준에 따라 그게 같다고 볼 수 있다면 True를 리턴하면 돼.

  1. ghci> let values = [-4.3, -2.4, -1.20.42.35.910.529.15.3, -2.4, -14.52.92.3]  
  2. ghci> groupBy (\x y -> (x > 0) == (y > 0)) values  
  3. [[-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]]  

 이로부터, 어떤 부분이 양수고 어떤 부분이 음수인지 명확하게 알 수 있어. 여기 적용된 동등성 함수는 두 개의 인자를 받아서 그 두개가 모두 음수거나 모두 양수일 때만 True를 리턴해. 이 동등성 함수는 \x y -> (x>0) && (y>0) || (x <= 0) && (y <= 0)이라고도 쓸 수 있지만, 내 생각엔 위 코드의 동등성 함수가 더 가독성이 좋아. By 함수를 위한 동등성 함수를 더 깔끔하게 쓰는 방법은 Data.Function 모듈의 on 함수를 포함시키는 거야. on은 이런 식으로 정의되어 있어.

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

 따라서 (==) `on` (>0)이라고 쓰는 건 \x y -> (x > 0) == (y > 0)이라고 쓰는 것과 같아. on은 종종 By함수와 함께 쓰여.

  1. ghci> groupBy ((==) `on` (> 0)) values  
  2. [[-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보다 큰 지 아닌 지에 따라 원소들을 묶어."

 이와 비슷하게, sort, insert, maximum과 minimum은 마찬가지로 더 일반적인 동등한 함수를 제공해. groupBy와 비슷한 함수들은 두 개의 원소를 받아서 그것들이 같은지 결정하는 함수를 인자로 받아. sortBy, insertBy, maximumBy, 그리고 minimumBy는 함수를 인자로 취해서 한 원소가 더 큰 지, 작은지, 혹은 서로 같은 지 결정하는 함수를 인자로 받아. sortBy의 타입 서명은 sortBy :: (a -> a -> Ordering) -> [a] -> [a]야. 이전에 다룬 걸 기억하고 있을지 모르겠지만, Ordering 타입은 LT, EQ, 또는 GT를 값으로 가질 수 있어. sort는 sortBy compare와 같은 함수야. 왜냐하면 compare는 그냥 Ord 타입 클래스에 속하는 타입을 가진 두 개의 인자를 받아서 그들의 크기 관계가 어떻게 되는 지 리턴하거든.

 리스트 역시 비교될 수 있지만, 이 건 사전순에 따라 비교돼. 리스트의 리스트가 있고 이걸 내부 리스트의 원소들에 따라서가 아니라, 그 리스트들의 길이에 따라 정렬하고 싶다면 어떻게 해야할까? 음, 대충 눈치챘겠지만 이럴 때 sortBy 함수를 이용할 수 있어.

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

 훌륭해! compare `on` length ... 봐, 이건 실제 영어랑 거의 비슷하게 읽을 수 있어! on 함수가 여기서 어떻게 동작하는 지 확신이 확신이 잘 안 되면, compare `on` length는 \x y -> length x `compare` length y와 같은 거라고 생각해. 동등성 함수를 사용하는 By 함수들을 다룰 때에는, (==) `on` something 을 보통 사용하게 되고, 정렬 함수를 취하는 By 함수들을 다룰 때에는 보통 compare `on` something 을 보통 사용하게 돼.


Data.Char


 Data.Char 모듈은 그 이름이 말하듯이, 문자를 다루는 함수들을 갖고 있어. 문자열이 문자로 이루어진 리스트기 때문에, 이 모듈에 포함된 함수들은 문자열에 대한 매핑이나 필터링에도 많은 도움이 돼. 

 Data.Char은 문자에 대한 술어를 엄청 많이 갖고 있어. 이 함수들은 문자를 하나 받아서 어떤 추측이 참인지 거짓인지를 돌려주지. 아래가 바로 이런 술어 함수들의 목록이야.

 isControl 은 문자가 제어 문자(control character)인지 아닌지 검사해.

 isSpace는 문자가 흰 공백(white-space) 문자인지 아닌지 검사해. 흰 공백 문자는 공백, 탭 글자, 개행 등등을 다 포함하는 거야.

 isLower는 해당 문자가 소문자인지 확인해.

 isUpper은 해당 문자가 대문자인지 확인해.

 isAlpha는 해당 문자가 알파벳인지 확인해.

 isAlphaNum은 해당 문자가 알파벳 또는 숫자인지 아닌지 확인해.

 isPrint는 문자가 출력 가능한 문자인지 확인해. 예를 들어서 제어 문자 같은 건 출력 가능한 문자가 아니지.

 isDigit은 해당 문자가 숫자(0~9)인지 아닌지 확인해.

 isOctDigit은 해당 문자가 8진수 숫자(0~7)인지 아닌지 확인해.

 isHexDigit은 해당 문자가 16진수 숫자(0~9, A~F)인지 아닌지 확인해.

 isLetter는 해당 문자가 알파벳인지 확인해.

 isMark는 해당 문자가 유니코드 마크 문자(Unicode mark character)인지 확인하기 위한 함수야. 유니코드 마크 문자는 선행 문자(preceding letters)와 형태 문자(form letters)를 억양(accent)을 줘ㅓ 합친 문자를 말해. 니가 프랑스인이라면 이걸 써.

 isNumber는 해당 문자가 숫자(numeric)인지 확인해.

 isPunctuation은 해당 문자가 구두점인지 확인해.

 isSymbol 은 해당 문자가 복잡한 수학기호 혹은 통화 기호(currency symbol)인지 확인해.

 isSeparator는 유니코드 공백과 구분자들을 확인하기 위한 함수야.

 isAscii는 해당 문자가 유니코드 문자 집합의 첫 128문자 안에 속하는 문자인지 확인해.

 isLatin1은 해당 문자가 유니코드의 첫 256개 문자 안에 속하는 문자인지 확인해.

 isAsciiUpper은 해당 문자가 Ascii문자이면서 대문자인지 확인해.

 isAsciiLower는 해당 문자가 Ascii문자이면서 소문자인지 확인해.

 이 모든 술어 함수들은 Char -> Bool이라는 타입 서명을 갖고 있어. 대부분의 경우 너는 이걸 문자열이나 혹은 그 비슷한 걸 필터링할 때 이 함수들을 사용하게 될거야. 자, 사용자의 이름을 입력받는 프로그램을 만들어보자. 이 때 사용자의 이름이 숫자와 문자로만 이루어져야한다면, Data.List 모듈의 함수와 all 함수를 조합해서 해당 사용자명이 적합한지 아닌지 판단할 수 있어.

  1. ghci> all isAlphaNum "bobby283"  
  2. True  
  3. ghci> all isAlphaNum "eddy the fish!"  
  4. False  

 아주 멋져. 까먹었을 까봐 말하는데, all 함수는 술어와 리스트르 발앋서 리스트의 모든 원소에 대해 해당 술어가 참일 때만 True를 리턴하는 함수야.

 isSpace를 이용해서 Data.List의 함수 words와 비슷한 함수를 구현할 수 있어.

 흐으으으음, 좋아, 이건 words가 하는 일과 비슷한 일을 하지만 공백으로만 구성된 원소를 남겨버려. 그럼 어떻게 해야할까? 물론, 당연히 저 거지같은 것들을 다 걸러내야겠지.

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

 아.

 Data.Char은 Ordering의 일종인 데이터 타입도 갖고 있어. Ordering 타입은 LT, EQ, 또는 GT 세 가지 값중 하나를 가질 수 있지. 이건 열거형의 일종이기도 해. 이 타입은 두 원소를 비교했을 때 나타날 수 있는 몇 가지 경우의 수를 기술하지. GeneralCategory 타입 역시 열거형이야. 이건 해당 문자가 속할 수 있는 몇 가지 카테고리들을 보여줘. 어떤 문자에 대한 GeneralCategory를 얻는 주 함수는 generalCategory야. 이 함수는 generalCategory :: Char -> GeneralCategory 타입을 갖고 있어. 이건 총 31가지의 타입을 갖고 있어서 여기 그걸 전부 적을 순 없지만, 대신에 몇 가지 이 함수를 이용한 예 정도는 살펴보자.

  1. ghci> generalCategory ' '  
  2. Space  
  3. ghci> generalCategory 'A'  
  4. UppercaseLetter  
  5. ghci> generalCategory 'a'  
  6. LowercaseLetter  
  7. ghci> generalCategory '.'  
  8. OtherPunctuation  
  9. ghci> generalCategory '9'  
  10. DecimalNumber  
  11. ghci> map generalCategory " \t\nA9?|"  
  12. [Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]  

 GeneralCategory 타입이 Eq 타입 클래스에 속하기 때문에, generalCategory c == Space 같은 구문도 사용할 수 있어.

 toUpper 함수는 문자를 대문자로 변경시켜. 공백, 숫자, 그리고 나머지 것들은 변하지 않아.

 toLower 함수는 문자를 소문자로 변경시켜.

 toTitle 함수는 해당 문자를 제목 문자(title-case)로 변경시켜. 대부분의 언어에서 제목 문자는 대문자랑 똑같애.

 digitToInt 함수는 문자를 Int 형으로 변경시켜. 이 함수가 성공하려면 문자는 반드시 '0'..'9' 또는 'a'..'f' 또는 'A'..'F'여야 해.

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

 intToDigit 함수는 digitToInt 함수의 반대 역할을 하는 함수야. 이건 0..15 범위 안에 있는 Int를 인자로 받아서 그걸 소문자로 바꿔 출력해.

  1. ghci> intToDigit 15  
  2. 'f'  
  3. ghci> intToDigit 5  
  4. '5'  

 ord와 chr 함수는 해당 문자를 각각 그에 합당하는 숫자로 바꾸거나, 그 반대로 동작해.

  1. ghci> ord 'a'  
  2. 97  
  3. ghci> chr 97  
  4. 'a'  
  5. ghci> map ord "abcdefgh"  
  6. [97,98,99,100,101,102,103,104]  

 두 문자의 ord 값의 차이는 그 둘이 유니코드 테이블에서 얼마나 멀리 떨어져 있는가를 나타내지.

 카이사르 암호는 알파벳의 각 문자를 고정된 숫자만큼 뒤로 미룬 문자로 바꿔침으로써 해당 메시지를 암호화하는 ㅊ초기의 암호화 기법이야. 이 암호화 기법과 비슷하지만, 문자를 알파벳에만 국한시키지 않는 우리만의 암호를 쉽게 만들 수 있어. 

  1. encode :: Int -> String -> String  
  2. encode shift msg = 
  3.     let ords = map ord msg  
  4.         shifted = map (+ shift) ords  
  5.     in  map chr shifted  

 자, 먼저 문자를 숫자의 리스트로 변경시켜. 그리고 나서 숫자를 문자로 다시 돌리기 전에 모든 숫자에 대해 일정한 값을 더해줘. 함수 합성계의 무법자가 되고 싶다면 함수의 본체를 map (chr . (+ shift) . ord) msg라고 적을 수도 있어. 이제 몇가지 메시지를 암호화해보자.

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

 좋아. 잘되는군. 암호를 푸는 건 기본적으로 똑같은 크기의 숫자를 더하는 대신 빼는 방식으로 동작하면 되겠지.

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


Data.Map


연관 리스트(딕셔너리라고도 불리는)는 정렬 순서를 신경 쓰지 않고, key와 value의 쌍을 저장하기 위해 사용되는 리스트들을 말해. 예를 들어서, 휴대폰 번호를 저장하기 위해 연관 리스트를 사용할 수 있을 거야. 사람의 이름을 키로 쓰고 번호를 값으로 쓰면 되니까. 그렇게 저장하면 그게 어떻게 정렬되어 있는 지 신경쓸 필요가 없어. 그냥 원하는 폰 번호를 얻기 위해선 그 번호 주인의 이름만 똑바로 입력하면 돼.

 Haskell에서 연관 리스트를 표현하기 위한 가장 명백한 방법은 페어의 리스트를 만드는 거야. 페어의 첫번째 요소는 키가 될거고, 두 번째 요소는 값이 되겠지. 여기 폰 번호의 연관 리스트 예제가 있어.

  1. phoneBook =   
  2.     [("betty","555-2938")  
  3.     ,("bonnie","452-2928")  
  4.     ,("patsy","493-2928")  
  5.     ,("lucille","205-2928")  
  6.     ,("wendy","939-8282")  
  7.     ,("penny","853-2492")  
  8.     ]  

 인덴트가 좀 이상하게 보일 수 있지만, 이건 그냥 문자열의 페어의 리스트일 뿐이야. 연관리스트를 다루는 가장 일반적인 동작은 어떤 값을 키를 이용해서 찾는 거야. 주어진 키로부터 값을 찾는 함수를 만들어보자.

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

 굉장히 단순하지. 이 함수는 키와 리스트를 인자로 받아서, 키와 일치하는 리스트의 원소만 남기고, 제일 첫번째 키 값 쌍의 값을 돌려줘. 하지만 키가 연관 리스트에 존재하지 않는다면 어떤 일이 발생할까? 흠, 여기서는 키가 존재하지 않는다면 텅 빈 리스트의 head를 취하려는 일이 생길 거고, 이건 런타임 에러를 발생시키겠지. 하지만, Maybe 데이터 타입을 이용하면 프로그램이 터지는 상황을 쉽게 피할 수 있어. 만약 키를 찾지 못한다면, 그 때 Nothing을 돌려주면 돼. 찾으면, 키에 맞는 어떤 값을 Just (어떤 값) 형태로 돌려주면 되지. 

  1. findKey :: (Eq k) => k -> [(k,v)] -> Maybe v  
  2. findKey key [] = Nothing  
  3. findKey key ((k,v):xs) = if key == k  
  4.                             then Just v  
  5.                             else findKey key xs  

 타입 서명을 봐. 이건 동등성 비교를 할 수 있는 키와 연관 리스트를 취해서, 아마도(maybe) 값을 생성해. 음, 맞는 말 같아. 

 이건 리스트에 사용할 수 있는 교과서적인 함수야. 경계조건, 리스트를 head와 tail로 나누기, 재귀 헤출, 모든게 들어가 있지. 이건 고전적인 접기 패턴이야. 따라서 접기를 이용해서 구현하는 방법도 한 번 보자.

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

 : 리스트에 대한 표준적인 재귀 패턴에서 명확하게 재귀를 쓰는 것보다 폴드를 사용하는 게 더 좋아. 왜냐하면 그게 더 읽기도 좋고 구분하기도 좋거든. foldr 호출은 누구나 보자마자 이게 접기라는 걸 알지만, 명확한 재귀호출을 읽는 건 좀 더 생각을 요구하거든.

  1. ghci> findKey "penny" phoneBook  
  2. Just "853-2492"  
  3. ghci> findKey "betty" phoneBook  
  4. Just "555-2938"  
  5. ghci> findKey "wilma" phoneBook  
  6. Nothing  

 매력적으로 동작하는군! 만약 여자의 휴대폰 번호가 있다면, 우린 단지(Just) 그걸 얻을 거고, 그렇지 않다면 아무 것도 못 얻겠지(Nothing).

 우린 지금 Data.List의 lookup 함수를 한 번 구현해본거야. 만약 키에 합당하는 값을 찾고 싶다면, 그걸 찾을 때까지 리스트의 모든 원소들을 순회해야만 하지. Data.Map 모듈은 훨씬 빠른 연관 리스트를 제공하고(얘네들은 내부적으로 트리를 써서 구현되어 있기 때문이야), 또 많은 유틸리티 함수들을 제공하지. 이제부터, 연관리스트 대신 맵을 써서 작업해볼거야.

 Data.Map이 Prelude와 Data.List의 함수랑 이름이 겹치는 함수를 많이 갖고 있기 때문에, 명시적 포함을 써야돼.


  1. import qualified Data.Map as Map  

 이 포함 구문을 스크립트에 넣고 GHCI를 이용해 스크립트를 불러와.

 이제 Data.Map이 우리를 위해 제공하는 것들을 살펴보자! 여기 그 함수들의 기본적인 설명이 있어.

 fromList 함수는 연관 리스트(리스트의 형태를 한)를 취해서 똑같은 관계를 가진 map을 리턴해.

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

 원래 연관 리스트에 중복된 키가 있었다면, 그 중복은 사라져. fromList의 타입 서명은

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

 이야. 이건 k와 v의 페어의 리스트를 인자로 받아서 타입 k의 키와 타입 v를 연결시킨 Map을 돌려줘.  연관리스트를 쓸 때는 키가 동등성 비교만 가능하면 됐지만(Eq 타입클래스의 타입이기만 하면 됐지), 여기서는 키가 정렬할 수도 있어야한다는 걸 주의해. 이건 Data.Map 모듈의 중요한 제약조건이야. 키가 정렬될 수 있어야 내부 트리에서 키를 재배치할 수 있거든.

 키가 Ord 타입 클래스의 멤버가 아니거나 한 게 아니면 키값 쌍을 쓸 때는 무조건 Data.Map을 써야할 거야.

 empty는 텅 빈 맵을 의미해. 이건 아무런 인자로 받지 않고, 그냥 텅 빈 맵을 돌려줘.

  1. ghci> Map.empty  
  2. fromList []  

 insert는 키와 값, 그리고 맵을 인자로 받아서 이전의 맵에 새로운 키값쌍이 삽입된 새로운 맵을 돌려줘.

  1. ghci> Map.empty  
  2. fromList []  
  3. ghci> Map.insert 3 100 Map.empty  
  4. fromList [(3,100)]  
  5. ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100  Map.empty))  
  6. fromList [(3,100),(4,200),(5,600)]  
  7. ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty  
  8. fromList [(3,100),(4,200),(5,600)]  

 fromList 함수는 텅 빈 리스트와 insert, fold를 이용해서 구현할 수 있어. 자, 봐봐.

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

 이건 굉장히 직관적인 접기야. 텅빈 맵에서 시작해서 리스트의 오른쪽부터 누산기에 키값쌍을 삽입해나가지.

 null은 맵이 텅 비었는 지 확인해.

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

 size는 맵의 크기를 말해줘.

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

 singleton은 키값쌍을 받아서 딱 그 하나의 인자만 갖고 있는 맵을 만들어.

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

 lookup은 Data.List의 lookup 함수와 유사하게 동작해. 다만 이건 맵에 대해서 동작하지. 이건 뭔가 찾으면 키에 대한 Just something을, 그렇지 않다면 Nothing을 리턴해.

 member는 키와 맵을 인자로 받아 해당 맵이 그 키를 포함하고 있는 지 아닌 지를 말해주는 술어야.

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

 mapfilter는 리스트에 대해 쓰는 동명의 함수와 거의 똑같이 동작해.

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

 toList는 fromList의 반대야.

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

 keyselems는 각각 키의 리스트와 값의 리스트를 반환해. keys는 map fst . Map.toList와 같고, elems는 map snd . Map.toList와 같아.

 fromListWith은 멋진 조그마한 함수야. 이건 fromList와 비슷하게 동작하지만, 다만 중복값을 그냥 제거하는 게 아니라, 그 중복값을 가지고 뭘할지 결정하기 위해 거기 적용할 함수를 사용해. 어떤 여자들은 많은 번호를 갖고 있고 그래서 연관 리스트를 이렇게 만들었다고 해보자.

  1. phoneBook =   
  2.     [("betty","555-2938")  
  3.     ,("betty","342-2492")  
  4.     ,("bonnie","452-2928")  
  5.     ,("patsy","493-2928")  
  6.     ,("patsy","943-2929")  
  7.     ,("patsy","827-9162")  
  8.     ,("lucille","205-2928")  
  9.     ,("wendy","939-8282")  
  10.     ,("penny","853-2492")  
  11.     ,("penny","555-2111")  
  12.     ]  

 이제 fromList를 이용해 이걸 map으로 만들어보자. 아마 많은 폰 번호를 유실하게 되겠지! 그래서, 이렇게 할거야.

  1. phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String  
  2. phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs  
  1. ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook  
  2. "827-9162, 943-2929, 493-2928"  
  3. ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook  
  4. "939-8282"  
  5. ghci> Map.lookup "betty" $ phoneBookToMap phoneBook  
  6. "342-2492, 555-2938"  

 만약 중복된 키가 발견되면, 인자로 넘긴 함수가 이 키에 대한 값들을 다른 어떤 값으로 합치는 것에 사용돼. 먼저 연관 리스트의 모든 값들을 원소가 하나인 리스트들로 만든 다음 ++ 연산을 이용해 그것들을 하나의 리스트로 합칠 수도 있지.

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

 굉장히 깔끔해! 다른 사용예로는, 숫자의 연관 리스트에서 키가 중복되는 게 있을 경우 그 값들 중에 가장 큰 값을 해당 키에 대한 값으로 하고 싶을 때 같은 게 있어.

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

 아니면 키가 같은 값들을 다 합친 걸 선택할 수도 있지.

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

 insertWith과 insert의 관계는 fromListWith과 fromList의 관계와 같아. 이건 키값 쌍을 맵에 집어넣지만, 만약 해당 맵이 이미 그 키를 포함하고 있다면, 인자로 같이 넘긴 함수를 이용해서 어떤 일을 할 지를 결정해.

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

 이건 Data.Map에 있는 함수들 중에서도 극히 일부야. 전체 함수 목록을 이 문서에서 확인할 수 있어.


Data.Set


Data.Set 모듈은, 음, 집합(Set)을 제공해. 수학에서의 집합이랑 비슷한 거야. 집합은 리스트와 맵의 관계랑 비슷한 거야. 집합의 모든 원소는 유일해. 그리고 이건 내부적으로 트리로 구현되어 있기 때문에(Data.Map의 map과 굉장히 비슷하지), 그 원소들은 정렬되어 있어. 회원증 검사, 삽입,삭제 등등은 똑같은 걸 리스트로 구현하는 것보다 집합으로 구현하는 게 훨씬 빨라. 집합을 다루는 가장 일반적인 연산은 뭔가를 집합에 삽는 것, 검사하는 것, 그리고 리스트를 집합으로 변환하는 것이야.

 Data.Set의 함수들 역시 Prelude와 Data.List 모듈의 이름들과 많이 겹치기 때문에, 명시적 포함을 써야 돼.

 이 포함 구문을 스크립트에 써.


  1. import qualified Data.Set as Set  

 그리고 GHCI로 이 스크립트를 불러와.

 두 개의 텍스트 조각을 갖고 있다고 하자. 이 두 개의 문장 모두에 쓰인 문자를 찾고 싶어.

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

 fromList 함수는 네가 생각한 거랑 거의 똑같이 동작할거야. 이건 리스트를 인자로 받아서 그걸 집합으로 바꿔줘.

  1. ghci> let set1 = Set.fromList text1  
  2. ghci> let set2 = Set.fromList text2  
  3. ghci> set1  
  4. fromList " .?AIRadefhijlmnorstuy"  
  5. ghci> set2  
  6. fromList " !Tabcdefghilmnorstuvwy"  

 위에서 볼 수 있듯이, 모든 원소들은 정렬되어 있고 또 유일해. 이제 둘 모두가 공유하는 게 뭔지 알아보기 위해 intersection 함수를 쓰자.

  1. ghci> Set.intersection set1 set2  
  2. fromList " adefhilmnorstuy"  

 첫 번째 집합에는 있지만 두 번째 집합에는 없는 원소가 뭔지, 혹은 그 반대는 또 어떤지를 알기 위해 difference 함수를 쓸 수 있어.

  1. ghci> Set.difference set1 set2  
  2. fromList ".?AIRj"  
  3. ghci> Set.difference set2 set1  
  4. fromList "!Tbcgvw"  

 혹은 두 문장에서 사용된 모든 유일한 문자들을 확인하기 위해 union 함수를 쓸 수 있지.

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

 null , size, member, empty, singleton, insert 그리고 delete 함수는 전부 네가 예측한 것처럼 동작해.

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

 또 부분집합이나 진부분집합을 검사할 수 있어. 집합 A의 모든 원소가 집합 B에 포함된 원소일 때 집합 A를 집합 B의 부분집합이라고 해. 그리고 집합 B가 집합 A의 모든 원소를 포함하고 있으며, 원소 개수가 집합 A보다 더 많을 때 집합 A를 집합 B의 진부분집합이라고 불러.

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

 map, filter 함수도 집합에 대해 사용 가능해.

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

 집합은 종종 fromList를 이용해서 집합으로 만듦으로써 리스트의 중복된 원소를 제거하고, 그걸 toList  함수를 이용해 리스트로 다시 바꾸는데에 사용돼. Data.List의 nub 함수가 이미 그 역할을 하지만, 아주 큰 리스트에서 중복된 원소를 제거하는 건 nub을 쓰는 것보다 위에서 언급한 방법을 쓰는 게 더 빨라. 하지만 nub은 list의 원소가 Eq 타입 클래스의 타입이라는 제약조건만 요구하는 반면에, 집합은 Ord 타입클래스의 타입이라는 제약조건까지 요구하지.

  1. ghci> let setNub xs = Set.toList $ Set.fromList xs  
  2. ghci> setNub "HEY WHATS CRACKALACKIN"  
  3. " ACEHIKLNRSTWY"  
  4. ghci> nub "HEY WHATS CRACKALACKIN"  
  5. "HEY WATSCRKLIN"  

 setNub은 일반적으로 큰 리스트에 대해서는 nub보다 빨라. 하지만 위에서 볼 수 있듯이, nub은 원소의 순서를 그대로 유지하는 반면에 setNub은 그렇지 않아.


 나만의 모듈 만들기


 지금까지 몇 가지 멋진 모듈들을 살펴봤어. 그러면, 어떻게 하면 우리만의 모듈을 만들 수 있을까? 거의 모든 프로그래밍 언어들은 코드를 여러 개의 파일로 분할할 수 있게 해주고, Haskell이라고 해서 다를 건 없어. 프로그램을 만들 때, 함수들과 타입들을 비슷한 목적을 가진 것들끼리 분류해서 여러 개의 모듈로 나누는 건 굉장히 좋은 연습이 돼. 이렇게 하면, 다른 함수들에서도 단순히 모듈을 포함시키는 걸 통해 이 함수들을 쉽게 재사용할 수 있게 되지.

 이제 몇가지 기하학적인 물체들의 겉넓이와 부피를 계산하는 몇 가지 함수를 제공하는 간단한 모듈을 만들어봄으로써 어떻게 자신만의 모듈을 만들 수 있는 지 살펴볼거야. 우선 Geometry.hs 라는 파일을 만드는 걸로 시작해보자.

 모듈은 함수를 내보내(export). 무슨 뜻이냐면 특정 모듈을 포함시켰을 때, 우리는 그 모듈이 내보내는(export) 함수들만 사용할 수 있어. 각 모듈은 내부적으로 쓰이는 함수를 정의할 수 있지만, 외부에서 쓸 수 있는 건 그 모듈이 내보내는 함수들과 타입들 뿐이야.

 모듈의 시작 지점에서, 모듈 이름을 명시할 수  있어. 파일 이름을 Geometry.hs로 했다면, 모듈 이름을 Geometry로 해야만 해. 그리고 나서, 그 뒤에 이 모듈이 내보낼 함수들을 명시하고, 그 다음 위치에서부터 함수를 작성하면 돼. 지금 만들 모듈은 그래서 이런 식으로 시작하게 될거야.

  1. module Geometry  
  2. ( sphereVolume  
  3. , sphereArea  
  4. , cubeVolume  
  5. , cubeArea  
  6. , cuboidArea  
  7. , cuboidVolume  
  8. where  

 아까 말한 것처럼 여기서는 구(sphere), 정육면체(cube), 직육면체(cuboid)의 겉넓이와 부피를 구할거야. 이제 진짜 함수들을 정의해보자.

  1. module Geometry  
  2. ( sphereVolume  
  3. , sphereArea  
  4. , cubeVolume  
  5. , cubeArea  
  6. , cuboidArea  
  7. , cuboidVolume  
  8. where  
  9.   
  10. sphereVolume :: Float -> Float  
  11. sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)  
  12.   
  13. sphereArea :: Float -> Float  
  14. sphereArea radius = 4 * pi * (radius ^ 2)  
  15.   
  16. cubeVolume :: Float -> Float  
  17. cubeVolume side = cuboidVolume side side side  
  18.   
  19. cubeArea :: Float -> Float  
  20. cubeArea side = cuboidArea side side side  
  21.   
  22. cuboidVolume :: Float -> Float -> Float -> Float  
  23. cuboidVolume a b c = rectangleArea a b * c  
  24.   
  25. cuboidArea :: Float -> Float -> Float -> Float  
  26. cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  
  27.   
  28. rectangleArea :: Float -> Float -> Float  
  29. rectangleArea a b = a * b  

 아주 표준적인 도형들이 바로 여기 있어. 그럼에도 불구하고 몇 가지 주목해야할 점이 있어. 정육면체는 직육면체에 속하기 때문에, 그 겉넓이와 부피를 모든 변의 길이가 같은 직육면체로 취급해서 정의했어. 또 rectangleArea라고 불리는, 변의 길이에 의거해서 넓이를 구하는 도우미 함수(helper function)를 정의했지. 사실 그냥 곱셈이랑 똑같아서 별 의미없긴 해. 눈여겨봐야할 부분은 이 함수를 모듈 내에서 사용하지만 (cuboidArea 함수와 cuboidVolume 함수에서) 이 함수를 밖으로 내보내진 않았다는 거야! 이 모듈이 세 가지 삼차원 도형을 다루는 함수들을 나타내는 모듈이기 때문에, rectangleArea를 사용함에도 불구하고 외부로 내보내진 않았어.

 모듈을 만들 때, 종종 모듈에 대한 인터페이스 역할을 하는 함수들만 내보내고 그 실제 구현은 숨기곤 해. 만약 누군가 이 Geometry 모듈을 쓴다면, 그 사람은 이 모듈이 내보내지 않는 함수들에 대해선 전혀 신경쓸 필요가 없어. 새로운 버젼에서 내부의 함수들을 완전히 바꿔버리거나 지우거나 하기로 결정해도(rectangleArea 함수를 없어고 그냥 * 연산을 사용할 수 있지) 아무도 그걸 신경쓰지 않을거야. 왜냐하면 애초부터 얘네들은 외부로 내보내질 않았으니까.

 이 모듈을 사용하기 위해선, 그냥 이렇게 쓰면 돼.

  1. import Geometry  

 Geometry.hs 파일을 같은 폴더에 넣고 프로그램이 이걸 포함하게 하면 돼.

 모듈은 계층 구조를 취할 수도 있어. 각각의 모듈은 여러 개의 부 모듈(sub-module)을 가질 수 있고, 또 그들 자신만의 부 모듈을 가질 수 있어. Geometry 모듈을 세 개의 부 모듈로 분리해서 각각의 모듈이 하나의 도형에 대한 함수들을 제공하도록 만들어보자.

 우선, Geometry라느 이름의 폴더를 만들어. G가 대문자라는 점에 주의해. 그리고, 그 안에 세 개의 파일을 집어넣어. 각각 Sphere.hs, Cuboid.hs, 그리고 Cube.hs라는 이름이야. 그리고 이 세가지의 파일이 포함하고 있어야할 내용은 다음과 같아.

 Sphere.hs

  1. module Geometry.Sphere  
  2. ( volume  
  3. , area  
  4. where  
  5.   
  6. volume :: Float -> Float  
  7. volume radius = (4.0 / 3.0) * pi * (radius ^ 3)  
  8.   
  9. area :: Float -> Float  
  10. area radius = 4 * pi * (radius ^ 2)  

 Cuboid.hs

  1. module Geometry.Cuboid  
  2. ( volume  
  3. , area  
  4. where  
  5.   
  6. volume :: Float -> Float -> Float -> Float  
  7. volume a b c = rectangleArea a b * c  
  8.   
  9. area :: Float -> Float -> Float -> Float  
  10. area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  
  11.   
  12. rectangleArea :: Float -> Float -> Float  
  13. rectangleArea a b = a * b  

 Cube.hs

  1. module Geometry.Cube  
  2. ( volume  
  3. , area  
  4. where  
  5.   
  6. import qualified Geometry.Cuboid as Cuboid  
  7.   
  8. volume :: Float -> Float  
  9. volume side = Cuboid.volume side side side  
  10.   
  11. area :: Float -> Float  
  12. area side = Cuboid.area side side side  

 다 됐어! 먼저 Geometry.Sphere를 보자. 이걸 Geometry라는 폴더 안에 위치시켰고 모듈 이름을 Geometry.Sphere로 지었다는 걸 명심하고. 똑같은 걸 직육면체에 대해서도 만들었지. 그리고 이 세 개의 부 모듈이 똑같은 이름으로 된 함수를 정의했다는 걸 잘 봐봐. 이 세 개가 서로 다는 모듈에 속하기 때문에 이런 식으로 정의할 수 있어. Geometry.Cuboid의 함수를 Geometry.Cube에서 사용할 수 있지만, 그냥 import Geometry.Cuboid라고 쓸 수는 없어. 이름이 겹치기 때문이지. 따라서 명시적 포함을 써야 잘 동작해.

 이제 이 세 개의 파일을 Geometry 폴더안에 같은 수준에서 위치시켰다면, 이렇게 해서 불러올 수 있어.

  1. import Geometry.Sphere  

 그러면 area 함수와 volume함수를 써서 구의 부피와 겉넓이를 구할 수 있지. 그리고 두 개 혹은 그 이상의 모듈들을 불러오고 싶다면, 각 모듈들의 함수 이름들이 겹치기 때문에 명시적 포함을 써야 돼. 그래서 이렇게 하면 되겠지.

  1. import qualified Geometry.Sphere as Sphere  
  2. import qualified Geometry.Cuboid as Cuboid  
  3. import qualified Geometry.Cube as Cube  

 그럼 Sphere.area, Sphere.volume, Cuboid.area 등등의 함수를 호출할 수 있어. 그리고 각각은 그에 합당하는 개체의 겉넓이나 부피를 계산해줄거야.

 언젠가 스스로 자신이 하나의 파일에 엄청난 양의 코딩을 하고 있으며 그 파일이 많은 함수들로 구성되어 있는 걸 발견하게 된다면, 그걸 어떤 공통의 목적을 가진 함수들로 분류하고 모듈로 분리할 수 있는 지 확인해봐. 그렇게 하면 이전과 같은 기능 중 일부를 요구하는 프로그램을 다음 번에는 단순히 모듈을 포함하는 것만으로 짤 수 있게 돼.