I'm wondering if it's possible to get all fields of a record in Haskell that end with a certain name. For example
data Record = Record {
field :: String
field2_ids :: Maybe [Int]
field3_ids :: Maybe [Int]
}
In this case I would like to get a list of fields ending with "ids". I don't know their names. I only know that they end with "ids" What I need is both the field name and the value it contains. So I guess this would be a list of maps
[{field2_ids = Maybe [Int]}, {fields3_ids = Maybe [Int]}...]
or even a list of tuples
[("field2_ids", Maybe [Int])...]
By the way, in my case the fields I'm extracting will always have the type of Maybe [Int]
.
Is this possible? I suspect that it's not possibly with vanilla record syntax but is this maybe something that could be achieved with lenses?
UPDATE
I understand my question is causing some confusion in terms of what I'm actually trying to do. So I will explain
I'm using a microservice pattern with Services. Each Service is tied to a single data model. For example a Blog Service would contain a single Blog model. But the Blog Service can have all kinds of relations. For example it can have a relation to a Category Service. It can also have a relation to a Tag Service. Since there is a possibility of having more than one relation with another Service I have the type of a Maybe [Int]
since I could be posting a Blog with Just [Int]
or Nothing
, no relations at all. Each Service handles its relations by registering them in a Relation table.
So to create a new Blog Post I need a data structure like this one in Servant
data BlogPostRequest = BlogPostRequest {
title :: String,
published :: DateTime,
public :: Bool,
category_ids :: Maybe [Int],
tag_ids :: Maybe [Int]
}
The endpoint will take all the fields related to the Blog model and store it as a new Blog instance. It will then take all the relations if present in category_ids and tag_ids and store it in the Relation table.
My only concern about this, using the traditional record syntax is that if I have multiple relations, the code will get very bloated. Services are generated from config files. So yes I do actually know all the names of the fields from start. I'm sorry my statement about this before was very confusing. My point was that if I could pull the fields out of the record just by knowing that their names end with _ids I could reduce a lot of the code.
This would be the vanilla record syntax approach. Imagine that storeRelation is a method which takes a String
and a Maybe [Int]
and handles storing the relation accordingly
createNewBlogPost post =
storeRelation "category" (category_ids post)
storeRelation "tag" (tag_ids post)
-- continue with rest of relations
This approach might not be so bad in the end. I would just add a new line for each relation. I was just wondering if there was a straight forward way to extract fields from records so that I could have the function like this
createNewBlogPost post =
storRelation $ extractRelations post
where storeRelation now takes a list of tuples and extractRelations is a function which extracts the fields ending with _ids
I’ve come up with a complicated solution using GHC.Generics
that seems to work. I’ve generalized the problem somewhat, writing a function with the following type signature:
fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
Specifically, this takes a value of type a
, which much be a record, and it produces a mapping from field names to values of type t
. Fields that have a type other than t
are ignored.
First, an example of what it does. Here’s your Record
type from your question, along with a sample value:
data Record = Record
{ field :: String
, field2_ids :: Maybe [Int]
, field3_ids :: Maybe [Int]
} deriving (Generic, Show)
exampleRecord :: Record
exampleRecord = Record
{ field = "a"
, field2_ids = Just [1, 2]
, field3_ids = Just [3, 4] }
Using fieldsDict
, it’s possible to get all the fields of type Maybe [Int]
:
ghci> fields exampleRecord :: M.Map String (Maybe [Int])
fromList [("field2_ids",Just [1,2]),("field3_ids",Just [3,4])]
To restrict the result to fields that end in _ids
, you can just filter the resulting map by its keys, which is left as an exercise to the reader.
I’ll be up-front: the implementation is not pretty. GHC.Generics
is not my favorite API, but at least it’s possible. Before we begin, we’ll need some GHC extensions:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
We’ll also need some imports:
import qualified Data.Map as M
import Data.Proxy
import GHC.Generics
import GHC.TypeLits
The hardest part of making this work is being able to analyze which fields are of the desired type. To help with this, we need a way to “cast” GHC.Generics
type representations, which we’ll represent with a separate class:
class GCast f g where
gCast :: f p -> Maybe (g p)
Unfortunately, implementing this is hard, since we need to perform case analysis on f
to see if it’s the same type as g
, and if it isn’t, product Nothing
. If we perform a naïve translation of that idea to typeclasses, we’ll end up with overlapping instances. To mitigate this problem, we can use a trick using closed type families:
type family TyEq f g where
TyEq f f = 'True
TyEq f g = 'False
instance (TyEq f g ~ flag, GCast' flag f g) => GCast f g where
gCast = gCast' (Proxy :: Proxy flag)
class GCast' (flag :: Bool) f g where
gCast' :: Proxy flag -> f p -> Maybe (g p)
instance GCast' 'True f f where
gCast' _ = Just
instance GCast' 'False f g where
gCast' _ _ = Nothing
Note that this means the GCast
class only has a single instance, but it’s still useful to keep gCast
as a class method instead of a free-floating function so that we can use GCast
as a constraint later.
Next, we’ll write a class that will actually analyze the GHC.Generics
representation of our record type:
class GFieldsDict f t where
gFieldsDict :: f p -> M.Map String t
This allows us to define our fieldsDict
function from earlier:
fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
fieldsDict = gFieldsDict . from
Now we just need to implement the instances of GFieldsDict
. To inform those instances, we can look at the expanded representation of Rep Record
:
ghci> :kind! Rep Record
Rep Record :: GHC.Types.* -> *
= D1
('MetaData "Record" "FieldsDict" "main" 'False)
(C1
('MetaCons "Record" 'PrefixI 'True)
(S1
('MetaSel
('Just "field")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 String)
:*: (S1
('MetaSel
('Just "field2_ids")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 (Maybe [Int]))
:*: S1
('MetaSel
('Just "field3_ids")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 (Maybe [Int])))))
Looking at this, we’ll need instances to drill down through D1
, C1
, and :*:
before we get at the actual fields. These instances are fairly simple to write, since they just defer to more nested parts of the type representation:
instance GFieldsDict f t => GFieldsDict (D1 md (C1 mc f)) t where
gFieldsDict (M1 (M1 rep)) = gFieldsDict rep
instance (GFieldsDict f t, GFieldsDict g t) => GFieldsDict (f :*: g) t where
gFieldsDict (f :*: g) = M.union (gFieldsDict f) (gFieldsDict g)
The actual functionality will go in an instance on S1
, since each S1
type corresponds to the individual record fields. This instance will use our GCast
class from earlier:
instance (KnownSymbol name, GCast f (Rec0 t)) => GFieldsDict (S1 ('MetaSel ('Just name) su ss ds) f) t where
gFieldsDict (M1 (rep :: f p)) = case gCast rep :: Maybe (Rec0 t p) of
Just (K1 v) -> M.singleton (symbolVal (Proxy :: Proxy name)) v
Nothing -> M.empty
…and that’s it. Is this complexity worth it? Probably not, unless you can hide it in a library somwehere, but this demonstrates that it’s possible.
Given that you actually do know all of the field names, and they are all of the same type, it should be quite a small amount of work to simply write each of the field names once, and much simpler than writing a big generic Template Haskell solution that would work for any data type.
A simple example:
idGetters :: [(String, Record -> Maybe [Int])]
idGetters = [("field2_ids", field2_ids),
("field3_ids", field3_ids)]
ids :: Record -> [(String, Maybe [Int])]
ids r = fmap (fmap ($ r)) idGetters
It looks a bit ugly, but that's simply the best way to work with the data structure you're presupposing.
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