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" )
まず 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"
電気タイプと返ってきました。いい感じです。
それでは 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"]
さらにいろいろリファクタリングします。
ポケモンタイプ / ポケモンを 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 などと比べても) 表現力が大きい。 それを少ない記述量で表現できるのはうれしい。