はじめに
注意
この記事ではbase
やsyb
よりコードを引用している.それらのコードはBSD-3-Clause
でライセンスされている.
.cabal
ファイル
cabal-version: 2.4 name: tmp-7DsxMlggVR version: 0.1.0.0 executable tmp-7DsxMlggVR main-is: Main.hs -- GHC 9.2.2 build-depends: base == 4.16.1.0 , syb == 0.7.2.1 hs-source-dirs: app default-language: Haskell2010
listify
の実行例
イルヴァの世界の方々の年齢に関してはイルヴァ資料館の情報を元にしている.
{-# LANGUAGE DeriveDataTypeable #-} module Main ( main ) where import Data.Data (Data) import Data.Generics.Schemes (listify) data World = World { worldName :: String , groups :: [Group] } deriving (Data) data Group = Group { groupName :: String , members :: [Member] } deriving (Data) data Member = Member { memberName :: String , anotherName :: String , age :: Maybe Int } deriving (Data, Show) worlds :: [World] worlds = [ World { worldName = "Ilva" , groups = [ Group { groupName = "Elea" , members = [ Member { memberName = "Romias" , anotherName = "The messenger from Vindale" , age = Just 24 } , Member { memberName = "Larnneire" , anotherName = "The listener of the wind" , age = Just 22 } ] } , Group { groupName = "People in Vernis" , members = [ Member { memberName = "Vessel" , anotherName = "The white hawk" , age = Just 31 } , Member { memberName = "Loyter" , anotherName = "The crimson of Zanan" , age = Just 32 } ] } ] } , World { worldName = "The world of Zakuzaku Actors" , groups = [ Group { groupName = "Hagure Queendom" , members = [ Member { memberName = "Derich" , anotherName = "The queen of Hagure Queendom" , age = Nothing } , Member { memberName = "Rosemary" , anotherName = "Big moss" , age = Nothing } ] } ] } ] main :: IO () main = print $ listify onlyMember worlds onlyMember :: Member -> Bool onlyMember _ = True
%cabal run Up to date [Member {memberName = "Romias", anotherName = "The messenger from Vindale", age = Just 24},Member {memberName = "Larnneire", anotherName = "The listener of the wind", age = Just 22},Member {memberName = "Vessel", anotherName = "The white hawk", age = Just 31},Member {memberName = "Loyter", anotherName = "The crimson of Zanan", age = Just 32},Member {memberName = "Derich", anotherName = "The queen of Hagure Queendom", age = Nothing},Member {memberName = "Rosemary", anotherName = "Big moss", age = Nothing}]
実装の確認
まずlistify
の実装を確認する.Hackageに載っているコードより引用.
-- | Get a list of all entities that meet a predicate listify :: Typeable r => (r -> Bool) -> GenericQ [r] listify p = everything (++) ([] `mkQ` (\x -> if p x then [x] else []))
if p x then [x] else []
の部分に関しては,リスト内包表記を用いることができるので,この関数は以下のように簡単になる.
listify :: Typeable r => (r -> Bool) -> GenericQ [r] listify p = everything (++) ([] `mkQ` (\x -> [x | p x]))
GenericQ
はData.Generics.Aliasesで定義されている.
type GenericQ r = forall a. Data a => a -> r
mkQ
の実装を確認する.Hackageに載っているコードより引用.
-- | Make a generic query; -- start from a type-specific case; -- return a constant otherwise -- mkQ :: ( Typeable a , Typeable b ) => r -> (b -> r) -> a -> r (r `mkQ` br) a = case cast a of Just b -> br b Nothing -> r
中置関数を定義する方法をこのように用いることはありなんか.ただ,とりあえず通常のように3つの引数を取る関数のように表記してみる.また,case
式はmaybe
を使うことで簡略化できる.
mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r mkQ r br a = maybe r br (cast a)
この関数については1に説明がある.以下,実行結果も含めて引用.
That is, the query (r ‘mkQ‘ q) behaves as follows when applied to an argument a: if a’s type is the same as q’s argument type, use q to interrogate a; otherwise return the default value r. To illustrate, here are some examples of using mkQ in an interactive session (recall that ord has type Char -> Int):
Prelude> (22 ‘mkQ‘ ord) ’a’ 97 Prelude> (22 ‘mkQ‘ ord) ’b’ 98 Prelude> (22 ‘mkQ‘ ord) True 22
a
の型が,br
が引数として取る値の型に等しければ,br a
を返し,そうでなければr
を返す.
ところで,cast
関数に関する面白い質問があるのでここに置いておく.
さて,先程のlistify
の定義に戻る.
listify :: Typeable r => (r -> Bool) -> GenericQ [r] listify p = everything (++) ([] `mkQ` (\x -> [x | p x]))
この場合,
foo :: (Typeable a, Typeable r) => (r -> Bool) -> a -> [r] foo p = [] `mkQ` (\x -> [x | p x])
は,型がa
の値v
を受け取って,それがp
が受け取る引数の型に等しく且つp v == True
ならば,[v]
を返す.そうでなければ[]
を返す.以下に実行例を示す.
ghci> foo even 3 [] ghci> foo odd 3 [3] ghci> foo odd 'c' []
次に,everything
の実装を確認する.まず実行例.
ageSum :: GenericQ Int ageSum = everything (+) (0 `mkQ` (fromMaybe 0 . age)) main :: IO () main = print $ ageSum worlds
%cabal run Up to date 109
age
がJust
の場合,それに含まれている年齢を使い,age
がNothing
,あるいはそもそも扱っている型がMember
でなければ0を使用する.そして,それらの和を計算する.
次に実装を確認する.Hackageに載っているコードより引用.
-- | Summarise all nodes in top-down, left-to-right order everything :: forall r. (r -> r -> r) -> GenericQ r -> GenericQ r -- Apply f to x to summarise top-level node; -- use gmapQ to recurse into immediate subterms; -- use ordinary foldl to reduce list of intermediate results -- everything k f = go where go :: GenericQ r go x = foldl k (f x) (gmapQ go x)
gmapQ
について確認する必要がある.
まずgmapQ
自体を試してみる.
main :: IO () main = print $ gmapQ (mkQ "" worldName) worlds
Up to date ["Ilva",""]
["Ilva", "The world of Zakuzaku Actors"]
とならなかったことに驚いたが,1によれば,gmapQ
は一階層しか操作せず,mkQ "" worldName
が最初のWorld
型の値と次の[World]
型の値に適用されるため,後者はデフォルト値の""
が返される.
続いて実装を確認する.Hackageに載っているコードより引用.
-- | A generic query that processes the immediate subterms and returns a list -- of results. The list is given in the same order as originally specified -- in the declaration of the data constructors. gmapQ :: (forall d. Data d => d -> u) -> a -> [u] gmapQ f = gmapQr (:) [] f
ちなみに[a]
に対しては定義が上書きされている.
gmapQ _ [] = [] gmapQ f (x:xs) = [f x,f xs]
gmapQr
について確認する.まず実行例.
main :: IO () main = putStrLn $ gmapQr (\x acc -> acc ++ " and " ++ x) "First name" (mkQ "Default name" worldName) worlds
%cabal run Up to date First name and Default name and Ilva
foldr
と似ている.
実装を確認する.Hackageに載っているコードより引用.
gmapQr :: forall r r'. (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> a -> r gmapQr o r0 f x0 = unQr (gfoldl k (const (Qr id)) x0) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
gfoldl
に関しては,既定の定義ではgfoldl = const id
となっている.しかし,deriving (Data)
で定義されたfoldl
は,大体以下のようになる.以下,ドキュメントより引用.
data T a b = C1 a b | C2 deriving (Typeable, Data) instance (Data a, Data b) => Data (T a b) where gfoldl k z (C1 a b) = z C1 `k` a `k` b gfoldl k z C2 = z C2
ちなみに[a]
に対しては以下のように定義されている.ソースコードより引用.
instance Data a => Data [a] where gfoldl _ z [] = z [] gfoldl f z (x:xs) = z (:) `f` x `f` xs
また,Qr
については以下のような定義になっている.
newtype Qr r a = Qr { unQr :: r -> r }
したがって,例えばC1 a b
に対してgmapQr
を適用すると以下のようになる.まず,gmapQr
の実装を再掲する.
gmapQr :: forall r r'. (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> a -> r gmapQr o r0 f x0 = unQr (gfoldl k (const (Qr id)) x0) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
x0
にC1 a b
を代入する.
gmapQr o r0 f (C1 a b) = unQr (gfoldl k (const (Qr id)) (C1 a b)) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
gfoldl
を適用する.
gmapQr o r0 f (C1 a b) = unQr (const (Qr Id) C1 `k` a `k` b) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
const (Qr id) C1
を適用する.
gmapQr o r0 f (C1 a b) = unQr (Qr id `k` a `k` b) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
始めのk
を適用する.
gmapQr o r0 f (C1 a b) = unQr (Qr (\r -> id (f a `o` r)) `k` b) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
id
を適用する.
gmapQr o r0 f (C1 a b) = unQr (Qr (\r -> f a `o` r) `k` b) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
更に,ラムダ式を簡単にして,以下のようになる.
gmapQr o r0 f (C1 a b) = unQr (Qr (f a `o`) `k` b) r0 where k :: Data d => Qr r (d->b) -> d -> Qr r b k (Qr c) x = Qr (\r -> c (f x `o` r))
続いて次のk
を適用する.
gmapQr o r0 f (C1 a b) = unQr (Qr (\r -> (f a `o`) (f b `o` r))) r0
括弧を払う.
gmapQr o r0 f (C1 a b) = unQr (Qr (\r -> f a `o` f b `o` r)) r0
ラムダ式を簡単にする.
gmapQr o r0 f (C1 a b) = unQr (Qr (f a `o` f b `o`)) r0
unQr
を適用して括弧を外す.
gmapQr o r0 f (C1 a b) = f a `o` f b `o` r0
このような感じになる.
gmapQ
の定義を再掲する.
gmapQ :: (forall d. Data d => d -> u) -> a -> [u] gmapQ f = gmapQr (:) [] f
簡約化したgmapQr
の結果において,o = (:)
,r0 = []
を代入するとこうなる.
gmapQ f (C1 a b) = gmapQr (:) [] f (C1 a b) = f a : f b : [] = [f a, f b]
なんとなくわかってきた.
everything
の定義を再掲する.
-- | Summarise all nodes in top-down, left-to-right order everything :: forall r. (r -> r -> r) -> GenericQ r -> GenericQ r -- Apply f to x to summarise top-level node; -- use gmapQ to recurse into immediate subterms; -- use ordinary foldl to reduce list of intermediate results -- everything k f = go where go :: GenericQ r go x = foldl k (f x) (gmapQ go x)
コメントにもあるように,まずfoldl
の第二引数の(f x)
で,トップレベルの値に関数f
を適用する.第三引数の(gmapQ go x)
で,x
の一階層のすべての子にgo x
を適用する.そしてアキュムレータの初期値をf x
として,gmapQ go x
というリストを関数k
を用いて畳み込む.
そしてlistify
の定義に戻る.
listify :: Typeable r => (r -> Bool) -> GenericQ [r] listify p = everything (++) ([] `mkQ` (\x -> [x | p x]))
listify onlyMember worlds
の場合を考える.
listify onlyMember worlds = everything (++) ([] `mkQ` (\x -> [x | onlyMember x]))
mkQ
によって生成される関数は,引数y
を取って,y
がMember
型の場合,(\x -> [x | onlyMember x])
を返す.onlyMember _ = True
なので,つまりこの関数は,y
がMember
型ならば[y]
という値を返す.そうでなければ[]
を返す.everything
によって,これはすべての階層で行われる.
そして,(++)
による畳込みで,すべて結合される.
おわりに
listify
なんとなくわかってきた.