6

How would I go about deriving the function

getField :: (Generic a, HasDatatypeInfo a) => Proxy (name :: Symbol) -> a -> b

to project a field from an arbitrary record using a type-level string (Symbol), using the generics-sop library?

This is similar to Retrieving record function in generic SOP, but I have the following problems:

  • The OP does not explain how to go the last mile to get the signature I desire.
  • The OP defines complex special-purpose helper types, which I am keen to avoid
  • The given solution only errors out at runtime, but compile-time matching should be possible, since a type-level DataTypeInfo is provided through the DatatypeInfoOf type family (nice to have, but not necessary).

The lens-sop package also seems to do something similar, but I can't work out how to make it work for me.

I would also prefer a solution that uses the IsProductType typeclass.

Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
Ari Fordsham
  • 2,437
  • 7
  • 28
  • The [`records-sop` package](https://hackage.haskell.org/package/records-sop) has most of this functionality, although I think it does not actually export the desired extraction function. (Oh, I see you made a PR in the meantime to do so.) – kosmikus Apr 08 '21 at 08:02
  • Annoyingly, `records-sop` is not very compatible with the other `generics-sop` idioms, like `IsProoductType` and the standard metadata, which I lean on a lot. – Ari Fordsham Apr 08 '21 at 08:34
  • 1
    I'm not sure why you're so interested in `IsProductType`. It's just a type synonym for some constraints that aren't sufficient for this task. – dfeuer Apr 08 '21 at 16:57

2 Answers2

1

I know this is a mess of an answer and not really what you were looking for, but it's the best I can do right now. Note that this works for both product types and sum types where all the constructors have the specified field name.

I think this could likely be simplified somewhat by separating the name lookup from the rest of the product handling. That is: use the datatype info to calculate the field number (as a unary natural), then use that number to dig through the code. Unfortunately, generics-sop doesn't seem to have really wonderful facilities for working with list zipping, so I ended up doing a lot "by hand".

{-# language EmptyCase, GADTs, TypeFamilies, DataKinds, TypeOperators, RankNTypes #-}
{-# language UndecidableInstances, UndecidableSuperClasses #-}
{-# language AllowAmbiguousTypes, TypeApplications, MultiParamTypeClasses,
  FlexibleContexts, FlexibleInstances, MagicHash, UnboxedTuples, ScopedTypeVariables #-}
{-# language ConstraintKinds #-}
{-# OPTIONS_GHC -Wall #-}

module Data.Proj where
import Data.Kind (Type, Constraint)
import Generics.SOP
import Generics.SOP.Type.Metadata as GST
import GHC.TypeLits
import Data.Type.Equality (type (==))

-- This is what you were looking for, but slightly more flexible.
genericPrj :: forall s b a.
  ( Generic a
  , HasFieldNS s b (GetConstructorInfos (DatatypeInfoOf a)) (Code a))
  => a -> b
genericPrj a = case genericPrj# @s a of (# b #) -> b

-- This version lets you force the *extraction* of a field without
-- forcing the field itself.
genericPrj# :: forall s b a.
  ( Generic a
  , HasFieldNS s b (GetConstructorInfos (DatatypeInfoOf a)) (Code a))
  => a -> (# b #)
genericPrj# a = case from a of
  SOP xs -> extraction @s @b @(GetConstructorInfos (DatatypeInfoOf a)) @(Code a) xs

-- | Extract info about the constructor(s) from 'GST.DatatypeInfo'.
type family GetConstructorInfos (inf :: GST.DatatypeInfo) :: [GST.ConstructorInfo] where
  GetConstructorInfos ('GST.ADT _ _ infos _) = infos
  GetConstructorInfos ('GST.Newtype _ _ info) = '[info]

class HasFieldNS (s :: Symbol) b (cis :: [GST.ConstructorInfo]) (code :: [[Type]]) where
  extraction :: NS (NP I) code -> (# b #)
instance HasFieldNS s b cis '[] where
  extraction x = case x of
instance (HasFieldNP' s b r c, HasFieldNS s b cis cs, rec ~ 'GST.Record q r, VerifyRecord rec)
    => HasFieldNS s b (rec ': cis) (c ': cs) where
  extraction (Z x) = extractIt @s @b @rec @c x
  extraction (S x) = extraction @s @b @cis @cs x

type family VerifyRecord rec :: Constraint where
  VerifyRecord ('GST.Record _ _) = ()
  VerifyRecord _ = TypeError ('Text "Constructor is not in record form.")

-- | Given info about a constructor, a list of its field types, and the name and
-- type of a field, produce an extraction function.
class HasFieldNP (s :: Symbol) b (ci :: GST.ConstructorInfo) (fields :: [Type]) where
  extractIt :: NP I fields -> (# b #)
instance (HasFieldNP' s b fi fields, ci ~ 'GST.Record _cn fi)
    => HasFieldNP s b ci fields where
  extractIt = extractIt' @s @_ @fi

class HasFieldNP' (s :: Symbol) b (fi :: [GST.FieldInfo]) (fields :: [Type]) where
  extractIt' :: NP I fields -> (# b #)

class TypeError ('Text "Can't find field " ':<>: 'ShowType s)
    => MissingField (s :: Symbol) where
  impossible :: a

instance MissingField s => HasFieldNP' s b fi '[] where
  extractIt' = impossible @s ()

instance HasFieldNP'' s b (fi == s) field fis fields =>
  HasFieldNP' s b ('GST.FieldInfo fi ': fis) (field ': fields) where
  extractIt' = extractIt'' @s @b @(fi == s) @field @fis @fields

class HasFieldNP'' (s :: Symbol) b (match :: Bool) (field :: Type) (fis :: [GST.FieldInfo]) (fields :: [Type]) where
  extractIt'' :: NP I (field ': fields) -> (# b #)
instance b ~ field => HasFieldNP'' _s b 'True field fis fields where
  extractIt'' (I x :* _) = (# x #)
instance (HasFieldNP' s b fis fields) => HasFieldNP'' s b 'False _field fis fields where
  extractIt'' (_ :* fields) = extractIt' @s @b @fis fields

Examples

data Foo
  = Foo {blob :: Int, greg :: String}
  | Bar {hello :: Char, blob :: Int}
deriveGeneric ''Foo

genericPrj @"blob" (Foo 12 "yo") ===> 12
genericPrj @"blob" (Bar 'x' 5) ===> 5
genericPrj# @"blob" (Bar 'x' 5) ===> (# 5 #)

myAbsurd :: Void -> a
myAbsurd = genericPrj @"whatever"

data Booby a
  = Booby {foo :: a}
  | Bobby {bar :: a}
deriveGeneric ''Booby

genericPrj @"foo" (Booby 'a')
-- Type error because Bobby has no foo field
dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • I was really looking to avoid `TemplateHaskell` altogether. – Ari Fordsham Apr 08 '21 at 08:37
  • @AriFordsham, I only used that to derive the `Generic` instances for the examples. You can instead derive `GHC.Generic` for each and give instances (or derive `anyclass`) the `Generic` and `HasDatatypeInfo` – dfeuer Apr 08 '21 at 12:13
  • There's got to be a simpler way. `records-sop` has a private function which does almost exactly this. I have to choose between rewriting it all, waiting for my pull request to go through to Hackage, or building a custom package. https://github.com/kosmikus/records-sop/pull/4 – Ari Fordsham Apr 09 '21 at 08:37
  • @AriFordsham, yes, I could strip this down to make it only work on single-constructor types, but I'm much more interested in rewriting it so I can get lenses and traversals. – dfeuer Apr 09 '21 at 08:40
1

As of version 0.1.1.0, records-sop provides this function:

getField :: forall s a b ra. (IsRecord a ra, IsElemOf s b ra) => a -> b

which needs the field name supplied as a type application rather than a proxy, like so:

data Foo = Foo { bar :: Int }

getField @"bar" (Foo 42) === 42

This provides compile-time extraction, although it will still need a bit of casting around to fit in with existing code in my project that manipulates standard generics-sop metadata.

This only works on single-constructor types. @dfeuer's answer also supports sum types.

Thank you @kosmikus, the coauthor of generics-sop and author of records-sop, for implementing this in response to this question!

Ari Fordsham
  • 2,437
  • 7
  • 28