Deriving projection functions using `generics-sop`
Asked Answered
B

2

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.

Bourges answered 7/4, 2021 at 18:41 Comment(3)
The records-sop package 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.)Harvison
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.Bourges
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.Fotheringhay
B
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!

Bourges answered 9/4, 2021 at 12:0 Comment(0)
F
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
Fotheringhay answered 8/4, 2021 at 1:6 Comment(4)
I was really looking to avoid TemplateHaskell altogether.Bourges
@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 HasDatatypeInfoFotheringhay
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. github.com/kosmikus/records-sop/pull/4Bourges
@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.Fotheringhay
B
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!

Bourges answered 9/4, 2021 at 12:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.