GHC Generics tools let you inspect constructor name, but what about field names?
Suppose I have a data type
data Foo
= F {f :: Int}
| OO {oo :: String}
| Foo {f :: Int, oo :: String}
And I have the following piece of data
aFoo :: Foo
I can write something like:
ooMay :: Foo -> Maybe String
ooMay f@(Foo {}) = Just (oo f)
ooMay f@(OO {}) = Just (oo f)
ooMay f@(F {}) = Nothing
guarding the accessor oo by the constructors which I know it is safe to use upon.
Is there a way to write this using Generics? Does something like fieldMay exist?
ooMay :: Foo -> Maybe String
ooMay f = fieldMay "oo" f
Yes, this is doable. The field names are written into the Rep as arguments to the M1 type constructor (the Meta argument). The Haddock glosses over M1 and the example Generic-based class ignores the information in it, but now we'll need it.
Just so we know what we're dealing with:
ghci> :kind! Rep Foo
Rep Foo :: * -> *
= D1
('MetaData "Foo" "Ghci1" "interactive" 'False)
(C1
('MetaCons "F" 'PrefixI 'True)
(S1
('MetaSel
('Just "f") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 Int))
:+: (C1
('MetaCons "OO" 'PrefixI 'True)
(S1
('MetaSel
('Just "oo")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 String))
:+: C1
('MetaCons "Foo" 'PrefixI 'True)
(S1
('MetaSel
('Just "f") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
(Rec0 Int)
:*: S1
('MetaSel
('Just "oo")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 String))))
So we basically just search for the S1s and pick the one with the correct name. For simplicity reasons, let's only look at the ones that have the right type beforehand.
class GFieldMay rep a where
gFieldMay :: String -> rep p -> Maybe a
-- fields of the right type might match the name
instance {-# OVERLAPS #-} Selector s => GFieldMay (M1 S s (K1 i a)) a where
gFieldMay name m@(M1 (K1 x))
| name == selName m = Just x
| otherwise = Nothing
-- any other fields must be pruned (no deep search)
instance {-# OVERLAPPING #-} GFieldMay (M1 S s f) a where
gFieldMay _ _ = Nothing
-- drill through any other metadata
instance {-# OVERLAPPABLE #-} GFieldMay f a => GFieldMay (M1 i m f) a where
gFieldMay name (M1 x) = gFieldMay name x
-- search both sides of products
instance (GFieldMay l a, GFieldMay r a) => GFieldMay (l :*: r) a where
gFieldMay name (l :*: r) = gFieldMay name l <|> gFieldMay name r
-- search the given side of sums
instance (GFieldMay l a, GFieldMay r a) => GFieldMay (l :+: r) a where
gFieldMay name (L1 x) = gFieldMay name x
gFieldMay name (R1 x) = gFieldMay name x
And that's that
fieldMay :: (Generic a, GFieldMay (Rep a) f) => String -> a -> Maybe f
fieldMay name = gFieldMay name . from
Ta-da!
main = putStr $ unlines $
[ show x ++ ": " ++ show (fieldMay "oo" x :: Maybe String)
| x <- [F 42, OO "5", Foo 42 "5"]]
-- F {f = 42}: Nothing
-- OO {oo = "5"}: Just "5"
-- Foo {f = 42, oo = "5"}: Just "5"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With