概要
多くのプログラミング言語でよく利用されMap型(ハッシュ型、キーバリュー型、辞書型、連想配列とも言われる)と、集合演算でよく使われるSet型をHaskellではどのように扱うかをいくつかのケースごとに紹介します。
Haskellでは標準関数としてMap型やSet型が実装されていないため、containersパッケージを事前にinstallする必要があります。
特徴
Map型、Set型は一般的にハッシュテーブルを用いて実装されている場合が多いがHaskellでは平衡二分木で実装されているため、データ構造の操作に関する計算量が異なる。
| ハッシュテーブル | 平衡二分木 |
---|
取得 | O(1) | O(logN) |
削除 | O(1) | O(logN) |
挿入 | O(1) | O(logN) |
計算量ではよく使われるハッシュテーブルに劣る反面、HaskellのMap型、Set型には数多くの強力な関数が用意されており、key-valueの順序関係も保持できるため、リストを扱うよう操作できる点がとても魅力的である。
HaskellのMapのベンチマーク結果はこちらを参照
参考資料
Data.Map-Hackage
Data.Set-Hackage
※本記事では上記の二つの記事の中から利用ケース別にいくつかピックアップして紹介するため、いくつか省いている機能(Map型における集合関数、本記事で載せている関数の類似関数など)が多数あります。より多くの関数や各種関数の計算量を知りたい方やは上記を参照してください。
version
本記事では0.6.2.1
を扱う。
準備
初めにそれぞれのパッケージをインポートします。
sample.hs
importqualifiedData.Map.StrictasMimportqualifiedData.SetasS
呼び出しの簡略化のため、ここではMap型をM、Set型をSとして利用します。
またimport qualified Data.Map
もありますが、特別な理由がなければData.Map.Strict
を使うことが推奨されているようです。
Data.Mapドキュメントの先頭行を抜粋
Note: You should use Data.Map.Strict instead of this module if:
- You will eventually need all the values stored.
- The stored values don't represent large virtual data structures to be lazily computed.
An efficient implementation of ordered maps from keys to values (dictionaries).
qualified
はPreludeの標準の関数と名前の衝突を避けるために使用します。
Map型
importqualifiedData.Map.StrictasM
以降、Map型がインポートされていることを前提に記述する。
使用するデータセット
fruits::[(String,Int)]fruits=[("apple",1),("orange",2),("banana",3),("peach",4),("cherry",5),("orange",6),("apple",7),("peach",8)]fruitsMap=M.fromListfruits
ghci環境での実行結果
PreludeM>fruits[("apple",1),("orange",2),("banana",3),("peach",4),("cherry",5),("orange",6),("apple",7),("peach",8)]PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]
Haskellでは既存の型からMap型を生成する場合、タプル型のリストが用いられ、fstの値がkey,sndの値がvalueに対応してMap型が生成される。Map型の生成にはfromList
が用いられる。
fromListの定義
fromList::Ordk=>[(k,a)]->Mapka
定義より、fstが比較可能な型で、2つの値を持つタプル型であれば変換可能であることがわかる。
また、変換時にfstが重複する場合、最後に表れたfstをkey,その値をvalueとした要素のみが残る。
※例ではfruitsに存在していた、("apple",1)("apple",7)
や("peach",4)("peach",8)
のうち、後の("apple",7)
や("peach",8)
のみが残っていることを表している。
同じキーが複数回表れた場合、何らかの処理を適用して前に表れた値についても利用する方法については後述する。
変換時はkeyで昇順ソートされてMap型に変換される。
※fromListの他にも昇順リスト、降順リストなど変換前のリストの状態に応じたMap型への変換関数が用意されているので、詳細はこちらを参照してください。
空Mapの表現(empty)
ghci環境での実行結果
PreludeMMain>M.fromList[]==M.emptyTruePreludeM>M.fromList[("orange",3)]==M.emptyFalse
単一要素を持ったMapの表現(singleton)
ghci環境での実行結果
PreludeM>M.singleton"peach"4==M.fromList[("peach",4)]TruePreludeM>M.singleton"peach"4==M.fromList[("peach",4),("oraneg",3)]False
データの参照(lookup,!?,!)
手続き型言語におけるMap.get(key)
やMap[key]
に相当する操作。
Data.Mapより抜粋
lookup::Ordk=>k->Mapka->Maybea
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.lookup"orange"fruitsMapJust6PreludeMMain>M.lookup"beef"fruitsMapNothing
Data.Mapより抜粋
(!?)::Ordk=>Mapka->k->Maybea
ghci環境での実行結果
PreludeMMain>fruitsMapM.!?"orange"Just6PreludeMMain>fruitsMapM.!?"beef"Nothing
ghci環境での実行結果
PreludeMMain>fruitsMapM.!"orange"6PreludeMMain>fruitsMapM.!"beef"***Exception:Map.!:givenkeyisnotanelementinthemapCallStack(fromHasCallStack):error,calledatlibraries/containers/containers/src/Data/Map/Internal.hs:627:17incontainers-0.6.2.1:Data.Map.Internal
lookup
や!?
はキーが存在しないことを考慮しているのに対し、!
は直接の値を取得しているため、存在しないときはエラーとなる
データの追加(insert)
手続き型言語におけるMap.set(key,value)
やMap[key]=value
に相当する操作。
Data.Mapより抜粋
insert::Ordk=>k->a->Mapka->Mapka
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.insert"grape"1fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("grape",1),("orange",6),("peach",8)]PreludeMMain>M.insert"orange"1fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",1),("peach",8)]PreludeMMain>M.insert"kiwi"10emptyfromList[("kiwi",10)]
データの挿入はkeyの昇順ソート順で適切な位置に挿入される。すでに存在しているキーに対してinsert
した場合は新しいkey,valueで上書きされる。
データの削除(delete)
Data.Mapより抜粋
delete::Ordk=>k->Mapka->Mapka
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.delete"orange"fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("peach",8)]PreludeMMain>M.delete"beef"fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]
存在しないものを削除しようとした場合は、そのままのMapを返す。
データの存在チェック(member)
手続き型言語におけるMap.has(key)に相当する操作。
Data.Mapより抜粋
member::Ordk=>k->Mapka->Bool
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.member"orange"fruitsMapTruePreludeMMain>M.member"beef"fruitsMapFalse
member
はkeyが存在する場合はTrue
,存在しない場合はFalse
を返す。
データ値の更新(update)
Data.Mapより抜粋
update::Ordk=>(a->Maybea)->k->Mapka->Mapka
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.update(\x->Just(x+100))"cherry"fruitsMapfromList[("apple",7),("banana",3),("cherry",105),("orange",6),("peach",8)]PreludeM>M.update(\x->Just(x+100))"beef"fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]
insert
による更新との違いは現在設定しているvalueを元に適用する操作を変えることができる。
keyリストの取得(keys)
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.keysfruitsMap["apple","banana","cherry","orange","peach"]
これはお馴染みの書き方。
valueリストの取得(elems)
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.elemsfruitsMap[7,3,5,6,8]
リストへの変換(toList)
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>toListfruitsMap[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]
toAscList
やtoDescList
を使えばソートも可能
キー、バリューリスト<(k,[a])
型>(fromListWith)への変換
Data.Mapより抜粋
fromListWith::Ordk=>(a->a->a)->[(k,a)]->Mapka
ghci環境での実行結果
PreludeM>fruits[("apple",1),("orange",2),("banana",3),("peach",4),("cherry",5),("orange",6),("apple",7),("peach",8)]PreludeM>M.fromListWith(++)$map(\(k,v)->(k,[v]))fruitsfromList[("apple",[7,1]),("banana",[3]),("cherry",[5]),("orange",[6,2]),("peach",[8,4])
fromListWith
ではMap型生成時にlambdaを渡せるため、keyが重複した場合の挙動を記述できる
重複したキーの値の和を求めたい場合には以下のようにも書ける
Data.Mapより抜粋
PreludeM>M.fromListWith(+)fruitsfromList[("apple",8),("banana",3),("cherry",5),("orange",8),("peach",12)]
高階関数の適用(map,foldl,filter)
Data.Mapより抜粋
map::(a->b)->Mapka->MapkbmapKeys::Ordk2=>(k1->k2)->Mapk1a->Mapk2afoldl::(a->b->a)->a->Mapkb->a
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.map(+1)fruitsMapfromList[("apple",8),("banana",4),("cherry",6),("orange",7),("peach",9)]PreludeM>M.mapKeys("F."++)fruitsMapfromList[("F.apple",7),("F.banana",3),("F.cherry",5),("F.orange",6),("F.peach",8)]PreludeM>M.foldl(+)0fruitsMap29PreludeM>M.foldlWithKey(\acckv->acc++(k++"="++showv))[]fruitsMap"apple=7banana=3cherry=5orange=6peach=8"PreludeM>M.filter(>5)(fruitsMap)fromList[("apple",7),("orange",6),("peach",8)]PreludeM>M.filterWithKey(\x_->lengthx>5)fruitsMapfromList[("banana",3),("cherry",5),("orange",6)]
リストで用意されているようなmap
,foldl
,foldr
,filter
がMap型にもそのまま適用できる。
key,value,またはその両方に適用できるような関数が各種に用意されている。
サイズを取得する(size)
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.sizefruitsMap5
keyの最小、最大を取得(lookupMax,lookupMin,findMax,findMin)
Data.Mapより抜粋
lookupMin::Mapka->Maybe(k,a)lookupMax::Mapka->Maybe(k,a)findMin::Mapka->(k,a)findMax::Mapka->(k,a)
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.lookupMinfruitsMapJust("apple",7)PreludeM>M.lookupMaxfruitsMapJust("peach",8)PreludeM>M.lookupMaxM.emptyNothingPreludeM>M.findMinfruitsMap("apple",7)PreludeM>M.findMaxfruitsMap("peach",8)PreludeM>M.findMaxM.empty***Exception:Map.findMax:emptymaphasnomaximalelementCallStack(fromHasCallStack):error,calledatlibraries/containers/containers/src/Data/Map/Internal.hs:1672:17incontainers-0.6.2.1:Data.Map.Internal
valueに対して最小、最大を取ってくるものは残念ながら存在しないため、事前にimport Data.turple (swap)
を用いて、key-valueを入れ替えておく必要がありそう。これはおそらく、Map型の定義より、valueには順序付きの制約(Ord
)がないためだと思われる。
keyからIndexを取得(lookupIndex,findIndex)
Data.Mapより抜粋
lookupIndex::Ordk=>k->Mapka->MaybeIntfindIndex::Ordk=>k->Mapka->Int
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.lookupIndex"cherry"fruitsMapJust2PreludeM>M.lookupIndex"beef"fruitsMapNothingPreludeM>M.findIndex"cherry"fruitsMap2PreludeM>M.findIndex"beef"fruitsMap***Exception:Map.findIndex:elementisnotinthemapCallStack(fromHasCallStack):error,calledatlibraries/containers/containers/src/Data/Map/Internal.hs:1457:23incontainers-0.6.2.1:Data.Map.Internal
keyを指定することで、その要素のindexを取得する。ハッシュテーブルで実装されたMapには順序関係は存在しないため、HaskellにおけるMap型ならでは機能である。
indexから要素(key-value)を取得(elemAt)
Data.Mapより抜粋
elemAt::Int->Mapka->(k,a)
ghci環境での実行結果
PreludeM>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeM>M.elemAt0fruitsMap("apple",7)PreludeM>M.elemAt3fruitsMap("orange",6)PreludeM>M.elemAt5fruitsMap***Exception:Map.elemAt:indexoutofrangeCallStack(fromHasCallStack):error,calledatlibraries/containers/containers/src/Data/Map/Internal.hs:1498:17incontainers-0.6.2.1:Data.Map.Internal
指定したindexに対応する要素をMapから取得する。ハッシュテーブルで実装されたMapには順序関係は存在しないため、HaskellにおけるMap型ならでは機能である。
Set型
以降、Set型がインポートされていることを前提に記述する
使用するデータセット
universeOne=["comet","earth","jupiter","mars","venus","mars","venus"]universeTwo=["star","jupiter","meteor","comet","planet"]universeOneSet=S.fromListuniverseOneuniverseTwoSet=S.fromListuniverseTwo
Map型と同様、fromList
を用いることでSet型に変換でき、変換時に昇順ソートされる。
PreludeS>universeOne["comet","earth","jupiter","mars","venus","mars","venus"]PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]
基本的な操作に対する関数名はMap型とほとんど同じものであるが、Set型についてもケース別に記載する
空Setの表現(empty)
ghci環境での実行結果
PreludeS>S.empty==S.fromList[]True
単一要素を持ったSetの表現(singleton)
ghci環境での実行結果
PreludeS>S.singleton"comet"==S.fromList["comet"]True
データの追加(insert)
Data.Setより抜粋
insert::Orda=>a->Seta->Seta
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.insert"cosmos"universeOneSetfromList["comet","cosmos","earth","jupiter","mars","venus"]
データの削除(delete)
Data.Setより抜粋
delete::Orda=>a->Seta->Seta
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.delete"mars"universeOneSetfromList["comet","earth","jupiter","venus"]
データの存在チェック(member)
Data.Setより抜粋
member::Ordk=>k->Mapka->Bool
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.member"jupiter"universeOneSetTruePreludeS>S.member"cosmos"universeOneSetFalse
リストへの変換(toList)
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.toListuniverseOneSet["comet","earth","jupiter","mars","venus"]
サイズを取得する(size)
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.sizeuniverseOneSet5
高階関数(map,filter,foldl)
Data.Setより抜粋
map::Ordb=>(a->b)->Seta->Setbfilter::(a->Bool)->Seta->Setafoldl::(a->b->a)->a->Setb->a
ghci環境での実行結果
PreludeS>S.map(\x->"U."++x)universeOneSetfromList["U.comet","U.earth","U.jupiter","U.mars","U.venus"]PreludeS>S.filter(\x->lengthx==5)universeOneSetfromList["comet","earth","venus"]PreludeS>S.foldl(++)[]universeOneSet"cometearthjupitermarsvenus"
Map型と同様にリストのように高階関数が取り扱える。
集合演算(union,difference,intersection)
Data.Setより抜粋
union::Orda=>Seta->Seta->Setadifference::Orda=>Seta->Seta->Setaintersection::Orda=>Seta->Seta->Seta
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>universeTwoSetfromList["comet","jupiter","meteor","planet","star"]PreludeS>S.unionuniverseOneSetuniverseTwoSetfromList["comet","earth","jupiter","mars","meteor","planet","star","venus"]PreludeS>S.differenceuniverseOneSetuniverseTwoSetfromList["earth","mars","venus"]PreludeS>S.intersectionuniverseOneSetuniverseTwoSetfromList["comet","jupiter"]
union、difference、intersectionはそれぞれ二つの集合の和集合、差集合、積集合した結果を返す。
部分集合かどうかチェック(isSubsetOf)
Data.Setより抜粋
isSubsetOf::Orda=>Seta->Seta->Bool
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.isSubsetOf(S.fromList["mars","earth"])universeOneSetTruePreludeS>S.isSubsetOf(S.fromList["mars","cosmos"])universeOneSetFalsePreludeS>S.isSubsetOfS.emptyuniverseOneSetTruePreludeS>S.isSubsetOfS.emptyS.emptyTruePreludeS>S.isSubsetOfuniverseOneSetuniverseOneSetTrue
isSubsetOf
はisSubsetOf A B
に対してAがBの部分集合である場合はTrue,そうでない場合はFalseを返す。
終わりに
本記事ではHaskellにおけるMap型、Set型で用意されている操作関数の中で、手続き型言語でもお馴染みの操作パターンを例にいくつか紹介しました。HaskellではMap型を用いるような操作はTurple型リストで解決できる場合が多いので、出番があるかどうかは作り手次第によると思いますが、Map型を利用する場合の名前確認表に本記事をご利用頂ければと思います。Map型を利用する場合は基本操作に対する計算量がO(1)ではなくO(logN)であること(操作によってはO(N)やO(NlogN)のものもあります)を十分考慮して設計してください。
また、公式ドキュメントには本記事で載せなかった亜種が多数存在しますので、よりニッチな利用ケースには参考資料の公式ページをご利用ください。