Quantcast
Channel: 連想配列タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 129

HaskellのMap型、Set型

$
0
0

概要

多くのプログラミング言語でよく利用され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

containers >= 0.5.9

本記事では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)

Data.Mapより抜粋
empty::Mapka
ghci環境での実行結果
PreludeMMain>M.fromList[]==M.emptyTruePreludeM>M.fromList[("orange",3)]==M.emptyFalse

単一要素を持ったMapの表現(singleton)

Data.Mapより抜粋
singleton::k->a->Mapka
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
Data.Mapより抜粋
(!)::Ordk=>Mapka->k->a
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)

Data.Mapより抜粋
keys::Mapka->[k]
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.keysfruitsMap["apple","banana","cherry","orange","peach"]

これはお馴染みの書き方。

valueリストの取得(elems)

Data.Mapより抜粋
elems::Mapka->[a]
ghci環境での実行結果
PreludeMMain>fruitsMapfromList[("apple",7),("banana",3),("cherry",5),("orange",6),("peach",8)]PreludeMMain>M.elemsfruitsMap[7,3,5,6,8]

リストへの変換(toList)

Data.Mapより抜粋
toList::Mapka->[(k,a)]
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)]

toAscListtoDescListを使えばソートも可能

キー、バリューリスト<(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)

Data.Mapより抜粋
size::Mapka->Int
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型

importData.SetasS

以降、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)

Data.Setより抜粋
empty::Seta
ghci環境での実行結果
PreludeS>S.empty==S.fromList[]True

単一要素を持ったSetの表現(singleton)

Data.Setより抜粋
singleton::a->Seta
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)

Data.Setより抜粋
toList::Seta->[a]
ghci環境での実行結果
PreludeS>universeOneSetfromList["comet","earth","jupiter","mars","venus"]PreludeS>S.toListuniverseOneSet["comet","earth","jupiter","mars","venus"]

サイズを取得する(size)

Data.Setより抜粋
size::Seta->Int
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

isSubsetOfisSubsetOf A Bに対してAがBの部分集合である場合はTrue,そうでない場合はFalseを返す。

終わりに

本記事ではHaskellにおけるMap型、Set型で用意されている操作関数の中で、手続き型言語でもお馴染みの操作パターンを例にいくつか紹介しました。HaskellではMap型を用いるような操作はTurple型リストで解決できる場合が多いので、出番があるかどうかは作り手次第によると思いますが、Map型を利用する場合の名前確認表に本記事をご利用頂ければと思います。Map型を利用する場合は基本操作に対する計算量がO(1)ではなくO(logN)であること(操作によってはO(N)やO(NlogN)のものもあります)を十分考慮して設計してください。
また、公式ドキュメントには本記事で載せなかった亜種が多数存在しますので、よりニッチな利用ケースには参考資料の公式ページをご利用ください。


Viewing all articles
Browse latest Browse all 129

Trending Articles