Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell reflection: does record have field?

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
like image 544
Dan Burton Avatar asked Mar 03 '23 03:03

Dan Burton


1 Answers

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"
like image 77
HTNW Avatar answered Mar 11 '23 10:03

HTNW