読者です 読者をやめる 読者になる 読者になる

きくらげ観察日記

好きなことを、適当に。

gunfoldの型を読み解く

Haskell

まず最初に、Data型クラスのその他のメソッドについて見てみましょう。

>>> :t toConstr
toConstr :: Data a => a -> Constr

toConstrは、値から値コンストラクタの情報を得るための関数です。

data Dish = Sushi { fish :: String }
          | Curry { isChicken :: Bool, spicyLevel :: Int }
          deriving (Eq, Show, Data, Typeable)

自前で定義したDish型に対して、toConstrを使ってみましょう。

>>> toConstr Sushi { fish = "Salmon" }
Sushi
>>> toConstr Curry { isChicken = False, spicyLevel = 2 }
Curry

さらに、取得したConstr型の値を用いて、さらにコンストラクタについての情報を得ることができます。

>>> let con = toConstr Curry { isChicken = False, spicyLevel = 2 }
>>> con
Curry
>>> constrFields con
["isChicken","spicyLevel"]
>>> constrIndex con
2

これらの情報から、CurryがDish型の2番目の値コンストラクタで、フィールドにisChickenとspicyLevelの2つを持つことがわかりました。


逆に、Constrから元の値に戻すこともできます。そのための関数がgunfoldです。
gunfoldの型を見てみましょう。

gunfold
  :: Data a =>
     (forall b r. Data b => c (b -> r) -> c r)
     -> (forall r. r -> c r) -> Constr -> c a

例によって第二引数は値コンストラクタをApplicative的なものに包むだけの関数です。

第一引数の型を見てみましょう。

app :: forall b r. Data b => c (b -> r) -> c r

第一引数appは、(n - 1)個まで引数が適用された値コンストラクタに、n個めの引数を適用する関数です。
コンストラクタへ渡す各引数の型がバラバラであるため、appの型も非常に一般的なものとなっています。

前回定義したPerson型を使って、gunfoldの動きを見てみましょう。

data Person = Person {
    name :: String
  , age :: Int
  , isMale :: Bool
  } deriving (Eq, Show, Data, Typeable)

appを使って順に引数を適用させていった時のイメージは以下のようになります。

c Person                    :: c (String -> Int -> Bool -> Person)
c (Person "Yamada")         :: c (Int -> Bool -> Person)
c (Person "Yamada" 20)      :: c (Bool -> Person)
c (Persor "Yamada" 20 True) :: c Person

このような動作をする関数を実際に作ってみましょう。

app :: forall c b r. (Monad c, Data b) => c (b -> r) -> c r
app con' = con' >>= \con -> do
  let rep = typeRep (Proxy :: Proxy b)
  if | rep == typeRep (Proxy :: Proxy String)
       -> return $ con $ unsafeCoerce "Yamada"
     | rep == typeRep (Proxy :: Proxy Int)
       -> return $ con $ unsafeCoerce (20 :: Int)
     | rep == typeRep (Proxy :: Proxy Bool)
       -> return $ con $ unsafeCoerce True

実行例:

>>> let con = toConstr $ Person "Tanaka" 15 True
>>> con
Person
>>> gunfold app Just con :: Maybe Person
Just (Person {name = "Yamada", age = 20, isMale = True})

この例では引数の型を調べるのに、Data.TypeableのtypeRepを使用しています。

gunfoldの簡単なバージョンとして、fromCotstr, fromConstrB, fromConstrMの3種類の関数が用意されています。

fromConstr :: Data a => Constr -> a
fromConstrB :: Data a => (forall d. Data d => d) -> Constr -> a
fromConstrM :: (Data a, Monad m) => (forall d. Data d => m d) -> Constr -> m a

fromConstrは値コンストラクタのみからなるフィールド無しの値を生成し、、fromConstrBは各フィールドの値を生成する関数を受け取りコンストラクタに適用します。fromConstrMはfromConstrBのモナド版です。

先ほどのappをfromConstrB用に書き換えると、以下のようになります。

fieldValue :: forall d. Data d => d
fieldValue
  | rep == typeRep (Proxy :: Proxy String)
  = unsafeCoerce "Yamada"
  | rep == typeRep (Proxy :: Proxy Int)
  = unsafeCoerce (20 :: Int)
  | rep == typeRep (Proxy :: Proxy Bool)
  = unsafeCoerce True
  where rep = typeRep (Proxy :: Proxy d)

実行例:

>>> fromConstrB fieldValue con :: Person
Person {name = "Yamada", age = 20, isMale = True}

Real World Haskell―実戦で学ぶ関数型言語プログラミング

Real World Haskell―実戦で学ぶ関数型言語プログラミング