Home About
Haskell

Haskell / 型クラスを定義して 関数名のバッティングを回避する

data とそのフィールド値の取得 でポケモン型を使いました。

そのとき疑問に思ったのが、 たとえば Pokemon をレコード構文で定義した場合に ポケモン型からその名前を取得するのに name aPokemon のようにすれば ポケモン名が取得できることがわかったのですが、name のような、よくありがちな関数名を使えるようにしたら、困るのでは?ということです。

つまり、次にたとえば 進化石 EvolutionStone 型を定義したとして、 そこにも name があったらバッティングして機能しなくなるよね? ということです。実際にやってみましょう。

Step1

この問題を確かめるために 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 とかに変更すれば良いわけですが。

Step 2

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 が重複しているとかのエラーは起きない。

Step 3

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

以上です。