Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic field lookup with records in Haskell

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

like image 217
Donna Avatar asked Jan 18 '18 15:01

Donna


2 Answers

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.

Usage example

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.

Implementation

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.

like image 165
Alexis King Avatar answered Oct 07 '22 11:10

Alexis King


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.

like image 28
amalloy Avatar answered Oct 07 '22 11:10

amalloy