data とそのフィールド値の取得 でポケモン型を使いました。
そのとき疑問に思ったのが、 たとえば Pokemon をレコード構文で定義した場合に ポケモン型からその名前を取得するのに name aPokemon のようにすれば ポケモン名が取得できることがわかったのですが、name のような、よくありがちな関数名を使えるようにしたら、困るのでは?ということです。
つまり、次にたとえば 進化石 EvolutionStone 型を定義したとして、 そこにも name があったらバッティングして機能しなくなるよね? ということです。実際にやってみましょう。
この問題を確かめるために Pokemon と EvolutionStone の型を定義します。 フィールド名はわざと name を重複させています。
pokemon.hs:
data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon { name :: String
, pokemonType :: PokemonType } deriving Show
data StoneType = ThunderStone | FireStone | WaterStone | LeafStone deriving Show
data EvolutionStone = EvolutionStone { name :: String
, stoneType :: StoneType } deriving Show
GHCi でロード:
$ ghci
> :load pokemon.hs
[1 of 1] Compiling Main ( pokemon.hs, interpreted )
pokemon.hs:20:40: error:
Multiple declarations of ‘name’
Declared at: pokemon.hs:15:30
pokemon.hs:20:40
|
20 | data EvolutionStone = EvolutionStone { name :: StoneName
| ^^^^
name が重複して宣言されている的なエラーが出ています。
まあ、この程度のコードなら pokemonName と stoneName とかに変更すれば良いわけですが。
Java 発想でいえば、クラスを定義した場合: (コードはGroovy)
enum PokemonType { NORMAL, WATER, ELECTRIC, FIRE }
enum StoneType { THUNDER, FIRE, WATER, LEAF }
class Pokemon {
String name
PokemonType type
}
class EvolutionStone {
String name
StoneType type
}
def pikachu = new Pokemon(name: "Pikachu", type: PokemonType.ELECTRIC)
println pikachu.name
def stone = new EvolutionStone(name: "Thunder", type: StoneType.THUNDER)
println stone.name
このようなコードを実行しても、当然 name が重複しているとかのエラーは起きない。
Haskell にも型クラスというものがあって、この問題を回避できる。 語弊があるのかもしれないが 型クラスは Javaで言うところのインタフェースのようなものとして考えられる。 つまり... Groovy でコードするとこんな感じ:
enum PokemonType { NORMAL, WATER, ELECTRIC, FIRE }
enum StoneType { THUNDER, FIRE, WATER, LEAF }
interface NamedObject {
String getName()
}
class Pokemon implements NamedObject {
String name
PokemonType type
@Override
String getName(){ return name }
}
class EvolutionStone implements NamedObject {
String name
StoneType type
@Override
String getName(){ return name }
}
ではこれを Haskell の型クラスで表現してみよう。
まず、Pokemon, EvolutionStone の data 定義でレコード構文は 使わない 。
data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon String PokemonType deriving Show
data StoneType = ThunderStone | FireStone | WaterStone | LeafStone deriving Show
data EvolutionStone = EvolutionStone String StoneType deriving Show
そして、下準備として toPokemonName, toStoneName というそれぞれの名前を得るための補助関数を定義:
toPokemonName :: Pokemon -> String
toPokemonName (Pokemon n _) = n
toStoneName :: EvolutionStone -> String
toStoneName (EvolutionStone n _) = n
そして、ついに 型クラス NamedObject を定義:
class NamedObject a where
name :: a -> String
name という関数を持つ必要があると定義している。
次に Pokemon, EvolutionStone を NamedObject のインスタンスにします。
instance NamedObject Pokemon where
name o = toPokemonName o
instance NamedObject EvolutionStone where
name o = toStoneName o
できました。 NamedObject のインスタンスになるには、 name 関数の定義が必要なので、toPokemonName, toStoneName を使って型に応じてそれぞれの名前を取得しています。
全体のコードを確認しておきましょう。
pokemon.hs:
data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon String PokemonType deriving Show
data StoneType = ThunderStone | FireStone | WaterStone | LeafStone deriving Show
data EvolutionStone = EvolutionStone String StoneType deriving Show
-- 補助関数
toPokemonName :: Pokemon -> String
toPokemonName (Pokemon n _) = n
toStoneName :: EvolutionStone -> String
toStoneName (EvolutionStone n _) = n
-- 型クラスの定義
class NamedObject a where
name :: a -> String
-- NamedObject インスタンス化
instance NamedObject Pokemon where
name o = toPokemonName o
instance NamedObject EvolutionStone where
name o = toStoneName o
早速、GHCi で確認:
> :reload
> pikachu = Pokemon "Pikachu" Electric
> pikachu
Pokemon "Pikachu" Electric
> name pikachu
"Pikachu"
うまくいきました。 name が重複しているとも言われず reload に成功。そして name pikachu で ポケモン名 "Pikachu" を得ることができました。
EvolutionStone はどうでしょうか?
> stone = EvolutionStone "LeafStone" LeafStone
> stone
EvolutionStone "LeafStone" LeafStone
> name stone
"LeafStone"
こちらも問題ありません。意図通り動いています。
さて、これらの補助関数 toPokemonName, toStoneName が無駄にトップレベルに存在しているのがいやですね。 そこをリファクタリングしましょう。
これは instance 定義を修正します。
instance NamedObject Pokemon where
name o = toPokemonName o where
toPokemonName (Pokemon n _) = n
instance NamedObject EvolutionStone where
name o = toStoneName o where
toStoneName (EvolutionStone n _) = n
このように トップレベルに定義していた toPokemonName, toStoneName を where 以下に入れただけです。 これらの関数をこの位置に書くのであれば、わざわざ toPokemonName と名付けず toName でいいでしょう。 つまり:
instance NamedObject Pokemon where
name o = toName o where
toName (Pokemon n _) = n
instance NamedObject EvolutionStone where
name o = toName o where
toName (EvolutionStone n _) = n
できました!
完成したコードを書き留めます。
pokemon.hs:
data PokemonType = Normal | Water | Electric | Fire deriving Show
data Pokemon = Pokemon String PokemonType deriving Show
data StoneType = ThunderStone | FireStone | WaterStone | LeafStone deriving Show
data EvolutionStone = EvolutionStone String StoneType deriving Show
class NamedObject a where
name :: a -> String
instance NamedObject Pokemon where
name o = toName o where
toName (Pokemon n _) = n
instance NamedObject EvolutionStone where
name o = toName o where
toName (EvolutionStone n _) = n
以上です。