Home About
Haskell , Groovy , Kotlin

Haskell の Map.fromList, Map.lookup の使い方

Haskell での Map 操作の覚え書き。 身近なポケモンを使ったコードをつくります。

Groovy で書くとこんなコード:

class Pair<T1,T2> {
    T1 first
    T2 second
    Pair(T1 first, T2 second){
        this.first = first
        this.second = second
    }
}

def pokemonList = [new Pair("Pikachu", "Electric"),
    new Pair("Eevee", "Normal"),
    new Pair("Charmander", "Fire"),
    new Pair("Electivire", "Electric"),
    new Pair("Squirtle", "Water")]


def findPokemonType = { List<Pair<String,String>> list, String name ->
    def pokemonNameTypeMap = [:]
    list.each { pair->
        pokemonNameTypeMap[pair.first] = pair.second
    }

    return pokemonNameTypeMap[name]
}

println findPokemonType( pokemonList, "Pikachu" )

Step1

まず pokemonList を Haskell で記述。 Groovy の Pair クラスは Haskell ではタプルを使うことにします。

maptest1.hs:

pokemonList :: [(String, String)]
pokemonList = [("Pikachu", "Electric"),
    ("Eevee", "Normal"),
    ("Charmander", "Fire"),
    ("Electivire", "Electric"),
    ("Squirtle", "Water")]

GHCi で確認:

$ ghci
> :load maptest1.hs
> pokemonList
[("Pikachu","Electric"),("Eevee","Normal"),("Charmander","Fire"),("Electivire","Electric"),("Squirtle","Water")]

最終的には ポケモン名からそのタイプを引ける findPokemonType 関数をつくりたいのですが、 最初のステップとして key が ポケモン名, value がポケモンタイプの pokemonNameTypeMap をつくります。

pokemonNameTypeMap :: Map.Map String String
pokemonNameTypeMap = Map.fromList pokemonList

Map を使うので import qualified Data.Map as Map しておく必要あり.

そして、この pokemonNameTypeMap にポケモン名を与えて ポケモンタイプを得るための findPokemonType 関数を定義:

findPokemonType :: String -> Maybe String
findPokemonType name = Map.lookup name pokemonNameTypeMap

Map.lookup が返すのは String そのものではなく Maybe String である点に注意!

コード全体(maptest1.hs)はこのようになります:

import qualified Data.Map as Map

pokemonList :: [(String, String)]
pokemonList = [("Pikachu", "Electric"),
    ("Eevee", "Normal"),
    ("Charmander", "Fire"),
    ("Electivire", "Electric"),
    ("Squirtle", "Water")]

pokemonNameTypeMap :: Map.Map String String
pokemonNameTypeMap = Map.fromList pokemonList

findPokemonType :: String -> Maybe String
findPokemonType name = Map.lookup name pokemonNameTypeMap

それでは GHCi で確認:

> :reload
> findPokemonType "Pikachu"
Just "Electric"

電気タイプと返ってきました。いい感じです。

Step2

それでは Step1 の maptest1.hs をリファクタリングします。

findPokemonType 関数を機能させるために作成していた pokemonNameTypeMap を一つにまとめます。

maptest2.hs:

import qualified Data.Map as Map

pokemonList :: [(String, String)]
pokemonList = [("Pikachu", "Electric"),
    ("Eevee", "Normal"),
    ("Charmander", "Fire"),
    ("Electivire", "Electric"),
    ("Squirtle", "Water")]

findPokemonType :: String -> Maybe String
findPokemonType name = Map.lookup name pokemonNameTypeMap
   where pokemonNameTypeMap = Map.fromList pokemonList

リファクタリングと言っても pokemonNameTypeMap を where に入れただけ・・・という。

GHCi で確認:

> :load maptest2.hs
> findPokemonType "Charmander"
Just "Fire"

なんとなく、 findPokemonType 関数内部で 関数の外側の pokemonList を利用しているのが気持ち悪い。 これを findPokemonType 関数の引数とするように修正:

findPokemonType :: [(String, String)] -> String ->  Maybe String
findPokemonType list name = Map.lookup name pokemonNameTypeMap
   where pokemonNameTypeMap = Map.fromList list

GHCi で確認:

> :load maptest2.hs
> findPokemonType pokemonList "Charmander"
Just "Fire"
> findPokemonType pokemonList "Eevee"
Just "Normal"

ポケモンタイプの検索のたびに pokemonList を入力したくなければ、 findPokemonType' 関数を定義すればよい:

> findPokemonType' = findPokemonType pokemonList
> :t findPokemonType'
findPokemonType' :: String -> Maybe String

これで、 findPokemonType' "ポケモン名" でポケモンタイプを取得できる。

> findPokemonType' "Eevee"
Just "Normal"

map を使って、複数のポケモン名からポケモンタイプを一度に引くこともできる。

> map (\pokemonName -> findPokemonType' pokemonName) ["Squirtle", "Electivire"]
[Just "Water",Just "Electric"]

Step3

さらにいろいろリファクタリングします。

ポケモンタイプ / ポケモンを data にする.

data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon (String, PokemonType) deriving Show

こうすると pokemonList はこうなる:

pokemonList :: [Pokemon]
pokemonList = [Pokemon ("Pikachu", Electric),
    Pokemon ("Eevee", Normal),
    Pokemon ("Charmander", Fire),
    Pokemon ("Electivire", Electric),
    Pokemon ("Squirtle", Water)]

findPokemonType はこれ:

findPokemonType :: [Pokemon] -> String -> Maybe PokemonType
findPokemonType list name = Map.lookup name pokemonNameTypeMap
   where pokemonNameTypeMap = Map.fromList fixList
         fixList = map (\pokemon -> ((toName pokemon), (toType pokemon))) list

Map.fromList fixList 部分の補足説明:
Map.fromList の型シグネチャを見ると Map.fromList :: Ord k => [(k, a)] -> Map.Map k a です。 つまり、 Map.fromList に与える リストは [(k, a)] になっていなければいけない。 しかし、現状は [Pokemon] になっている。 それで map (\pokemon -> ((toName pokemon), (toType pokemon))) pokemonList することで [Pokemon][(PokemonName, PokemonType)] のリストに変換している。

data として Pokemon と PokemonType を定義したことで、findPokemonType の型シグネチャが読みやすくなった。 ポケモンリストと文字列を引数として、Maybe ポケモンタイプ が戻ると読める。

もっと 型シグネチャ をわかりやすくしたければ、String に対して type (型シノニム) PokemonName を追加しよう:

type PokemonName = String

そうすれば、findPokemonType の型シグネチャがこうなる:

findPokemonType :: [Pokemon] -> PokemonName -> Maybe PokemonType

これで findPokemonType 関数の処理内容の説明が 型シグネチャだけで表現できた。

ちなみに、Kotlin で表現すると:

fun findPokemonType(pokemonList: List<Pokemon>, pokemonName: String): Optional<PokemonType> {
   ...
}

これはこれで読みやすいのですが、Haskell の型シグネチャを見てしまうともはや Kotlin 冗長が過ぎると感じてしまいます。

更に data Pokemon の定義も修正:

data Pokemon = Pokemon (PokemonName, PokemonType) deriving Show

これで data Pokemon の定義がポケモン名とポケモンタイプのタプルであることが明確になった。

あとは、data Pokemon から ポケモン名とポケモンタイプをそれぞれ取り出す toName, toType 関数を用意:

toName :: Pokemon -> PokemonName
toName (Pokemon v) = fst v

toType :: Pokemon -> PokemonType
toType (Pokemon v) = snd v

これで全部用意できました。

全体をまとめる maptest3.hs:

import qualified Data.Map as Map

type PokemonName = String
data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon (PokemonName, PokemonType) deriving Show


pokemonList :: [Pokemon]
pokemonList = [Pokemon ("Pikachu", Electric),
    Pokemon ("Eevee", Normal),
    Pokemon ("Charmander", Fire),
    Pokemon ("Electivire", Electric),
    Pokemon ("Squirtle", Water)]

findPokemonType :: [Pokemon] -> PokemonName -> Maybe PokemonType
findPokemonType list name = Map.lookup name pokemonNameTypeMap
   where pokemonNameTypeMap = Map.fromList fixList
         fixList = map (\pokemon -> ((toName pokemon), (toType pokemon))) list

toName :: Pokemon -> PokemonName
toName (Pokemon v) = fst v

toType :: Pokemon -> PokemonType
toType (Pokemon v) = snd v

GHCi を起動して確認:

> :load maptest3.hs
> findPokemonType pokemonList "Pikachu"
Just Electric

まとめ

Haskell の記述は最初はコンパクトすぎて読みづらいと思っていたのですが、 慣れてくると、シンプルかつ強力に感じてきました。 しかも、単純なだけでなく (Kotlin などと比べても) 表現力が大きい。 それを少ない記述量で表現できるのはうれしい。