Возможное решение проблемы существующих / несуществующих полей с использованием вычислений на уровне типа.
Некоторые необходимые расширения и импорт:
{-# LANGUAGE DeriveGeneric, ScopedTypeVariables, DataKinds, KindSignatures,
TypeApplications, TypeFamilies, TypeOperators, FlexibleContexts #-}
import Data.Aeson
import Data.Proxy
import GHC.Generics
import GHC.TypeLits
Вот тип данных (будет использоватьсяповышен), который указывает, отсутствует какое-либо поле или присутствует.Также семейство типов, которое отображает отсутствующие типы в ()
:
data Presence = Present
| Absent
type family Encode p v :: * where
Encode Present v = v
Encode Absent v = ()
Теперь мы можем определить параметризованную запись, содержащую все возможные поля, например:
data Foo (a :: Presence)
(b :: Presence)
(c :: Presence) = Foo {
field1 :: Encode a Int,
field2 :: Encode b Bool,
field3 :: Encode c Char
} deriving Generic
instance (FromJSON (Encode a Int),
FromJSON (Encode b Bool),
FromJSON (Encode c Char)) => FromJSON (Foo a b c)
Одна проблема: записьполный тип для каждой комбинации вхождений / отсутствий будет утомительным, особенно если каждый раз присутствует только несколько полей.Но, возможно, мы могли бы определить синоним вспомогательного типа FooWith
, который позволит нам упомянуть только те поля, которые присутствуют:
type family Mentioned (ns :: [Symbol]) (n :: Symbol) :: Presence where
Mentioned '[] _ = Absent
Mentioned (n ': _) n = Present
Mentioned (_ ': ns) n = Mentioned ns n
-- the field names are repeated as symbols, how to avoid this?
type FooWith (ns :: [Symbol]) = Foo (Mentioned ns "field1")
(Mentioned ns "field2")
(Mentioned ns "field3")
Пример использования:
ghci> :kind! FooWith '["field2","field3"]
FooWith '["field2","field3"] :: * = Foo 'Absent 'Present 'Present
Еще одна проблема: для каждогозапрос, мы должны повторить список обязательных полей два раза: один в URL («fields = a, b, c ...») и другой в ожидаемом типе.Было бы лучше иметь единый источник правды.
Мы можем вывести список полей уровня терминов, которые будут добавлены в URL, из списка полей уровня типов, используя вспомогательный класс типов Demote
:
class Demote (ns :: [Symbol]) where
demote :: Proxy ns -> [String]
instance Demote '[] where
demote _ = []
instance (KnownSymbol n, Demote ns) => Demote (n ': ns) where
demote _ = symbolVal (Proxy @n) : demote (Proxy @ns)
Например:
ghci> demote (Proxy @["field2","field3"])
["field2","field3"]