すごいH たのしく学ぼう!

第6章 モジュール
twitter: @wrist
facebook: ohashi.hiromasa
自己紹介
• twitter ID: @wrist (手首)
• http://hiromasa.info
• 岐阜(高校) → 名古屋(大学) → 大阪(社会人)
• 現在社会人3年目
• 某メーカーにて音響信号処理の研究開発やってます
(宣伝)大阪PRML読書会
• 機械学習の勉強会を主催してます
• 現在二本立て
• Pythonによるデータ分析入門
• PRML読書会
• 詳しくは「大阪 PRML読書会」
でググってください
• 次回6/22(日)
「第6章:モジュール」の概要
• モジュール(概要、インポート方法)
• 標準モジュールの関数で問題を解く
• 単語頻度カウント
• リストの包含判定
• 暗号生成・解読
• 正格な左畳み込み
概要続き
• かっこいい数を見つけよう
• Maybe, Just, Nothing
• 連想リスト
• タプルを用いた表現
• Data.Map
• モジュールの作成
• 階層型モジュール
モジュールとは
• いくつかの関数や型、型クラスなどを定義したファイル
• Haskellのプログラムはモジュールの集合
• 定義したものは外部へエクスポート可能
コードを複数モジュールに分ける利点
• 多くの異なるプログラムから使える
• 疎結合となることで再利用性が高まる
• 管理しやすい
Haskellの標準ライブラリ
• 複数のモジュールに分割
• モジュールごとに共通の目的を持つ関数、型が定義
• ex. リスト操作、並行プログラミング、複素数
• これまでの章のすべての関数、型、型クラスは

全てPreludeというモジュールの一部
GHCiのプロンプト
• デフォルトだと”Prelude> “となっているが、

これはPreludeモジュールを使っているという意味
• GHCi上で”:m + Data.List”などと他のモジュールをイン
ポートすると、表示は”Prelude Data.List> “に変化
• :show promptで表示、:set promptで変更可能
GHCi上では:show importsで
importしたモジュールが見れる
GHCi> :m + Data.List Data.Map
GHCi> :show imports
import Prelude -- implicit
import Data.List
import Data.Map
モジュールのインポート
• import ModuleName でインポート
• 例: リスト中の一意な要素を数える関数の作成
• Data.Listに含まれるnub関数を使用
• nub: Listから重複を取り除く関数
import Data.List
!
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub
Hoogle
• Haskellの検索エンジン
• 関数名、モジュール名、型シグネチャから検索可能
GHCiでのインポート
• ghci> :m + Data.List
• ghci> :m + Data.List Data.Set -- 複数モジュール
• モジュールをインポートするスクリプトを

ロードした場合は:m +を使う必要はない
特定の関数のみインポート
• 特定の関数のみインポート
• import Data.List (nub, sort)
• 特定の関数以外をインポート
• import Data.List hiding (nub) -- nub以外をインポート
修飾付きインポート(qualifiedインポート)
• 名前の競合を避けるための機構
• import qualified Data.Map
• Data.Mapのfilter関数を使う場合

Data.Map.filterとアクセスする必要有
• import qualified Data.Map as M
• M.filterでアクセスできる
.演算子
• 二つの用法
• 修飾付きインポートしたモジュールの関数の参照
• 関数合成演算子
• モジュール名と関数名の間に空白を空けずに置いた場合
インポートされた関数、そうでなければ関数合成
標準モジュールの関数で問題を解く
• 標準ライブラリのモジュールに含まれる関数の紹介
• 実例を見ていく
単語を数える
• 文字列に対して単語をカウントしたい
• Data.Listのwords関数: 空白で単語を区切る
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 sentence"
["hey","these","are","the","words","in","this","sentence"]
• Data.Listの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]]
要素が隣接していない場合
• あらかじめソートすることで解決
• Data.Listのsort関数を利用
GHCi> group ["boom", "bip", "bip", "boom", "boom"]
[["boom"],["bip","bip"],["boom","boom"]]
GHCi> sort [5,4,3,7,2,1]
[1,2,3,4,5,7]
GHCi> sort ["boom", "bip", "bip", "boom", "boom"]
["bip","bip","boom","boom","boom"]
文字列を単語リストに分割しソートしグルーピ
ングし単語と出現回数のタプルに分ける関数
GHCi> :t words
words :: String -> [String]
GHCi> :t sort
sort :: Ord a => [a] -> [a]
GHCi> :t group
group :: Eq a => [a] -> [[a]]
GHCi> :t (ws -> (head ws, length ws))
(ws -> (head ws, length ws)) :: [a] -> (a, Int)
import Data.List
!
wordNums :: String -> [(String,Int)]
wordNums = map (ws -> (head ws, length ws)) . group . sort . words
GHCi> wordNums "wa wa wee wa"
[("wa",3),("wee",1)]
関数合成なしで書く
• うわあ括弧だらけ!
import Data.List
!
wordNums :: String -> [(String,Int)]
wordNums xs = map (ws -> (head ws, length ws)) (group (sort (words xs)))
干し草の山から針を探す
• 2つのリストを受け取り、1つ目のリストが2つ目のリストの
どこかに含まれているかを調べる
• [3,4]は[1,2,3,4,5]に含まれているが[2,5]は含まれていない
• 検索対象のリストをhaystack(干し草の山)、

検索したいリストをneedle(針)と呼ぶ
方針
• Data.Listのtails関数を使いリストの全tailを取得
!
• Data.ListのisPrefixOfを使い

2つ目のリストが1つ目のリストで始まっているかを調べる
!
• Data.Listのanyで要素のどれかが述語を満たすかを調べる
GHCi> tails “party" => ["party","arty","rty","ty","y",""]
GHCi> tails [1,2,3] => [[1,2,3],[2,3],[3],[]]
GHCi> "hawaii" `isPrefixOf` "hawaii joe” => True
GHCi> "haha" `isPrefixOf` “ha" => False
GHCi> "ha" `isPrefixOf` “ha" => True
GHCi> any (> 4) [1,2,3] => False
GHCi> any (=='F') "Frank Sobotka” => True
GHCi> any (x -> x > 5 && x < 10) [1,4,11] => False
組み合わせる
• Data.ListにisInfixOfという同じ動作の関数が!

ちくしょう!
import Data.List
!
isIn :: (Eq a) => [a] -> [a] -> Bool
needle `isIn` haystack = any (needle `isPrefixOf`) (tails haystack)
GHCi> "art" `isIn` "party"
True
GHCi> [1,2] `isIn` [1,3,5]
False
シーザー暗号サラダ
• シーザー暗号でメッセージを暗号化
• 文字列をアルファベット上で一定の数だけシフト
• Unicode文字全体に対するものが作れる
• Data.Charの関数を使用(ord関数, chr関数)
GHCi> :m + Data.Char
GHCi> ord ‘a' => 97
GHCi> chr 97 => 'a'
GHCi> map ord "abcdefgh"
=> [97,98,99,100,101,102,103,104]
シフトする数と文字列を受け取り文字列中の各文字を

アルファベット上で指定された数だけ前方向にシフトする関数
• encode関数
import Data.Char
!
encode :: Int -> String -> String
encode offset msg = map (c -> chr $ ord c + offset) msg
!
-- (chr . (+ offset) . ord)でも可
GHCi> encode 3 "hey mark” => "kh|#pdun"
GHCi> encode 5 "please instruct your men” => "uqjfxj%nsxywzhy%~tzw%rjs"
GHCi> encode 1 "to party hard” => "up!qbsuz!ibse"
decode関数
import Data.Char
!
encode :: Int -> String -> String
encode offset msg = map (c -> chr $ ord c + offset) msg
!
decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
GHCi> decode 3 "kh|#pdun"
"hey mark"
GHCi> decode 5 "uqjfxj%nsxywzhy%~tzw%rjs"
"please instruct your men"
GHCi> decode 1 "up!qbsuz!ibse"
"to party hard"
foldl使用時のスタックオーバーフロー
• foldlはメモリの特定の領域を使い過ぎた時に起こる

スタックオーバーフローエラーを引き起こすことがある
GHCi> foldl (+) 0 (replicate 100 1)
100
GHCi> foldl (+) 0 (replicate 1000000 1)
1000000 -- H本だとここでstack overflow
GHCi> foldl (+) 0 (replicate 10000000 1)
10000000
GHCi> foldl (+) 0 (replicate 100000000 1)
-- 手元のマシンだとここで固まる(まさに外道)
stack overflowの理由
• Haskellは遅延評価であり実際の計算は

可能な限り後まで引き伸ばされる
• foldlを使う時、各ステップにおいて

アキュムレータの計算を実際には行わない
• 新しい計算で前の結果を参照するかもしれないので

メモリ上に先延ばしにした計算を保持し続ける
• メモリを使い果たしてスタックオーバーフロー
foldlの計算の様子
• 100万要素あると先延ばしにしていた計算が

全て再帰的に行われるので

スタックオーバーフローを引き起こす
foldl (+) 0 [1,2,3] =
foldl (+) (0 + 1) [2,3] =
foldl (+) ((0 + 1) + 2) [3] =
foldl (+) (((0 + 1 ) + 2) + 3) [] =
((0 + 1) + 2) + 3 =
(1 + 2) + 3 =
3 + 3 =
6
計算を先延ばしにしないfoldl
• 左折り畳みの各ステップ間で計算が遅延されず

すぐに評価されるfoldl —> 正格なfoldl
foldl' (+) 0 [1,2,3] =
foldl' (+) 1 [2,3] =
foldl' (+) 3 [3] =
foldl' (+) 6 [] =
6
Data.Listのfoldl’
• foldlの正格なバージョン
• foldl1に足してもfoldl1’という正格なバージョンが存在
GHCi> :m + Data.List
GHCi> foldl' (+) 0 (replicate 100000000 1)
100000000
• 通りを歩いていると老婦人が近付いて来て言いました
• 「各桁の数の合計が40になる最初の自然数は何か」
• 123ならば1+2+3=6, では40になる数は?
かっこいい数を見つけよう
方針
• 数を引数として受け取り各桁の合計を求める関数を作る
• showを使って数を文字列に変換
• Data.CharのdigitToIntを使って

文字を数に変換
• これらを使って関数を書く
• 各桁の合計が40になる最初の数を探す関数を作る
• Data.Listのfind関数を使う
GHCi> show 124
"124"
GHCi> :m + Data.Char
GHCi> digitToInt '2'
2
GHCi> digitToInt 'F'
15
GHCi> digitToInt 'z'
*** Exception: 

Char.digitToInt: not a digit 'z'
各桁の合計を求める関数
import Data.Char
import Data.List
!
digitSum :: Int -> Int
digitSum = sum . map digitToInt . show
GHCi> :t show
show :: Show a => a -> String
GHCi> :t (map digitToInt)
(map digitToInt) :: [Char] -> [Int]
GHCi> :t sum
sum :: Num a => [a] -> a
GHCi> :t (sum . (map digitToInt) . show)
(sum . (map digitToInt) . show) :: Show a => a -> Int
digitSumを適用した結果が

40となる最初の数を探す関数
• Data.Listのfind関数を使う
!
• 第一引数は述語、第二引数はリスト
• Maybeとは?
GHCi> :t find
find :: (a -> Bool) -> [a] -> Maybe a
Maybe a型
• Maybe a型の値は0個か、ちょうど1個の要素だけを持てる
• リスト型[a]が0個、1個、あるいはもっと沢山の要素を

持てるのに似ている
• 失敗する可能性があることの表現に使用
Maybe a型の値
• NothingかJust xのどちらか
• Nothing
• 0個の要素を持っている = 何も持っていない という値
• 空リストに似ている
• Just x
• Maybe a型のxを保持していることを表現
よく分からないので型を調べる
GHCi> Nothing
Nothing
GHCi> :t Nothing
Nothing :: Maybe a
GHCi> Just "hey"
Just "hey"
GHCi> Just 3
Just 3
GHCi> :t Just "hey"
Just "hey" :: Maybe [Char]
GHCi> :t Just 3
Just 3 :: Num a => Maybe a
GHCi> :t Just True
Just True :: Maybe Bool
findの動作
• 述語を満たす要素が見つかったら、その要素をJustで
ラップしたものが返される
• 見つからなければNothingが返される
GHCi> :t find
find :: (a -> Bool) -> [a] -> Maybe a
GHCi> find (> 4) [3,4,5,6,7]
Just 5
GHCi> find odd [2,4,6,8,9]
Just 9
GHCi> find (=='z') "mjolnir"
Nothing
各桁の合計が40になる

最初の数を見つける関数
• 先程実装したdigitSum関数とfindを組み合わせる
import Data.List
import Data.Char
!
digitSum :: Int -> Int
digitSum = sum . map digitToInt . show
!
firstTo40 :: Maybe Int
firstTo40 = find (x -> digitSum x == 40) [1..]
GHCi> firstTo40
Just 49999
合計値をnとして一般化
import Data.List
import Data.Char
!
digitSum :: Int -> Int
digitSum = sum . map digitToInt . show
!
firstTo :: Int -> Maybe Int
firstTo n = find (x -> digitSum x == n) [1..]
GHCi> firstTo 27
Just 999
GHCi> firstTo 1
Just 1
GHCi> firstTo 13
Just 49
キーから値へのマッピング
• 集合のようなデータを扱うときは順序を気にしない
• 連想リストのキー
• 例:ある住所に誰か住んでいるかを調べる
• 住所で名前を検索したい
• この節の話
• 望みの値(誰かの名前)を何らかのキー(その人の住所)で検索
連想リストの表現
• 2つの方法が紹介
• ペア(タプル)のリストとして表現
• [(“betty”, “555-2938”), (“bonnie”, “452-2928”), …]
• 線形に走査する必要有
• Data.Map
• 高速な検索が可能
ペアのリストで表現
• 与えられたキーに対して値を検索する関数
findKey :: (Eq k) => k -> [(k, v)] -> v
findKey key xs = snd . head . filter ((k, v) -> key == k) $ xs
GHCi> let phoneBook = [("betty", "555-2938"), 

("bonnie", "452-2928"), ("patsy", "493-2928"), ("lucille", "205-2928"),
("wendy", "939-8282"), ("penny", "853-2492")]
GHCi> findKey "betty" phoneBook
"555-2938"
GHCi> findKey "bety" phoneBook
"*** Exception: Prelude.head: empty list
検索関数の改良
• Maybe型を使う
findKey :: (Eq k) => k -> [(k, v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs)
| key == k = Just v
| otherwise = findKey key xs
• 畳み込みを用いた表現
findKey :: (Eq k) => k -> [(k, v)] -> Maybe v
findKey key xs = foldr
((k, v) acc -> if key == k then Just v else acc)
Nothing xs
実行例
GHCi> findKey "betty" phoneBook
Just "555-2938"
GHCi> findKey "penny" phoneBook
Just "853-2492"
GHCi> findKey "betty" phoneBook
Just "555-2938"
GHCi> findKey "wilma" phoneBook
Nothing
• このfindKey関数はData.Listのlookup関数と同じ
Data.Map
• ペアのリストで連想リストを表現するとキーに対応した値が見つかるまで
すべての要素を走査する必要有
• Data.Mapモジュールに高速な連想リストが存在
• ここからは「連想リストを使う」と言う代わりに「Mapを使う」と表す
import
• Data.MapはPreludeやData.Listと競合する名前をエクス
ポートしているので修飾付きインポートする
• import qualified Data.Map as Map
• このimport文をスクリプトに書いて、

それからそのスクリプトをGHCiでロード
Data.MapのfromList関数
• 連想リストを受け取り同じ対応関係を持つMapを返す
• 連想リストのように表示されるがもはや連想リストではない
GHCi> Map.fromList [("betty", "555-2938"), ("bonnie", "452-2928"), ("patsy", "493-2928"),
("lucille", "205-2928"), ("wendy", "939-8282"), ("penny", "853-2492")]
Loading package array-0.5.0.0 ... linking ... done.
Loading package deepseq-1.3.0.2 ... linking ... done.
Loading package containers-0.5.5.1 ... linking ... done.
fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928"),
("patsy","493-2928"),("penny","853-2492"),("wendy","939-8282")]
特徴
• 重複したキーがあった場合、後のほうの要素が使われる
GHCi> Map.fromList [("MS",1),("MS",2),("MS",3)]
fromList [("MS",3)]
• 型シグネチャ
• キーの型クラスがOrdであり順序比較が必要
• 高速化の要因
GHCi> :t Map.fromList
Map.fromList :: Ord k => [(k, a)] -> Map.Map k a
phoneBookのMapによる実装
import qualified Data.Map as Map
!
phoneBook :: Map.Map String String
phoneBook = Map.fromList $ [("betty" , "555-2938"),
("bonnie" , "452-2928"),
("patsy" , "493-2928"),
("lucille", "205-2928"),
("wendy" , "939-8282"),
("penny" , "853-2492")]
Map.lookupによる検索
GHCi> :t Map.lookup
Map.lookup :: Ord k => k -> Map.Map k a -> Maybe a
GHCi> Map.lookup "betty" phoneBook
Just "555-2938"
GHCi> Map.lookup "wendy" phoneBook
Just "939-8282"
GHCi> Map.lookup "grace" phoneBook
Nothing
新しい番号を挿入して新しいMapを作る
GHCi> :t Map.insert
Map.insert :: Ord k => k -> a -> Map.Map k a -> Map.Map k a
GHCi> Map.lookup "grace" phoneBook
Nothing
GHCi> let newBook = Map.insert "grace" "341-9021" phoneBook
GHCi> Map.lookup "grace" newBook
Just "341-9021"
サイズを調べる
GHCi> :t Map.size
Map.size :: Map.Map k a -> Int
GHCi> Map.size phoneBook
6
GHCi> Map.size newBook
7
電話番号を文字列ではなくIntのリストとして表現
• “939-8282”ではなく[9,3,9,8,2,8,2]として表現したいが-が邪魔
• Data.CharのisDigitを使う
GHCi> :t isDigit
isDigit :: Char -> Bool
GHCi> :t filter
filter :: (a -> Bool) -> [a] -> [a]
GHCi> :t digitToInt
digitToInt :: Char -> Int
import Data.Char
!
string2digits :: String -> [Int]
string2digits = map digitToInt . filter isDigit
GHCi> :t (filter isDigit)
(filter isDigit) :: [Char] -> [Char]
GHCi> :t (map digitToInt)
(map digitToInt) :: [Char] -> [Int]
GHCi> :t (map digitToInt) . (filter isDigit)
(map digitToInt) . (filter isDigit) :: [Char] -> [Int]
GHCi> string2digits "948-9282"
[9,4,8,9,2,8,2]
phoneBookをstring2digitsでマップ
GHCi> let intBook = Map.map string2digits phoneBook
GHCi> :t intBook
intBook :: Map.Map String [Int]
GHCi> Map.lookup "betty" intBook
Just [5,5,5,2,9,3,8]
電話帳の拡張
• キーの重複があるとfromListは重複を削除
• fromListWithを使う
• 重複時の振る舞いを決められる
Map.fromListWith
• 重複時に番号を結合
!
• 重複時にリストとして結合
!
• maxで最大値、(+)で値の和などを取ることも可能
phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith add xs
where add number1 number2 = number1 ++ ", " ++ number2
phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith
(++) $ map ((k, v) -> (k, [v])) xs
モジュールを作ってみよう
• 再利用性を高めるためにモジュールに分ける
• モジュールからは関数をエクスポートする
• モジュールをインポートすると

そのモジュールがエクスポートする関数が利用可能
• モジュール内部だけで使える関数も定義可能
幾何学モジュール
• Geometoryモジュールの作成
• Geometry.hsというファイル名にする
• 先頭でモジュール名を指定
• module Geometry
• その次にエクスポートする関数名を列挙
• (sphereVolume, sphereArea, …, cuboidVolume) where
• その後で関数を定義
階層的モジュール
• Geometryを分割して立体の種類ごとに分割
• Geometryというディレクトリを作成
• その中に3つのファイルSphere.hs, Cuboid.hs,
Cube.hsを作る
• Geometryディレクトリのあるディレクトリ上のファイル
からimport Geometry.Sphereなどとimport可能

すごいHaskell読書会 第六章 発表資料