Manera idiomática de definir tipos nuevos versus tipos persistentes en Haskell

Resuelto Razumov asked hace 7 meses • 0 respuestas

Tengo un tipo que representa un registro persistente. Quiero tener un tipo muy similar que represente datos que deben publicarse para crear un nuevo registro.

Este es el tipo completo:

data Record = Reading
  { id: UUID
  , value: String
  ...
  }

el tipo "nuevo" es el mismo menos el "id", que será generado automáticamente por la base de datos. ¿Cómo puedo definir este tipo? Estoy usando un servidor para definir la API.

Mi estrategia actual es anteponer el tipo y todos los campos con "nuevo", lo cual funciona pero es redundante para modelos de muchos campos. También he visto la estrategia anidada donde tengo un tipo compartido común. También pensé en hacer que la identificación sea opcional, pero realmente no quiero poder publicarla.

Razumov avatar Feb 17 '24 07:02 Razumov
Aceptado

Puede implementar esto con un enfoque similar al de datos de tipo superior .

Primero, algunas importaciones:

{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Example where

import Data.Functor.Identity
import Data.Proxy
import Data.UUID 
import Data.Aeson
import GHC.Generics

Luego, defina un registro con un parámetro de tipo superior:

data Record f = Record  { recordId :: f UUID, recordValue :: String } deriving (Generic)

El IdentityFunctor le ofrece una variante de este registro que siempre tiene una identificación.

type RecordWithId = Record Identity

El uso Maybele brinda una variante con una identificación opcional .

type RecordWithOptionalId = Record Maybe 

Proxyse puede utilizar como Functor con un único valor de "unidad" poco interesante. (y sin valores del tipo envuelto). Esto nos permite crear un tipo para Recordsin ID.

type RecordWithoutId = Record (Proxy)

Podemos derivar Showpara nuestro Record.

deriving instance (Show (f UUID)) => Show (Record f)

Se requiere pasar omitNothingFields = Truey allowOmitedFields = Trueen los AesoncasosRecordWithoutId para analizar a como era de esperar. Esto requiere una versión de Aeson >= 2.2.0.0 (que al momento de escribir este artículo es más reciente que la última Stackage Snapshot). Probablemente podría implementar las instancias de Aeson a mano si esta versión no funciona para usted.

instance (ToJSON (f UUID)) => ToJSON (Record f) where
    toJSON = genericToJSON defaultOptions { omitNothingFields = True, allowOmitedFields = True }

instance (FromJSON (f UUID)) => FromJSON (Record f) where
    parseJSON = genericParseJSON defaultOptions { omitNothingFields = True, allowOmitedFields = True }

Codificando un valor con un ID:

ghci> import qualified Data.ByteString.Lazy.Char8 as BL
ghci> BL.putStrLn $ encode (Record {recordId = Identity nil, recordValue = "value" })
{"recordId":"00000000-0000-0000-0000-000000000000","recordValue":"value"}

Codificando un valor sin ID:

ghci> BL.putStrLn $ encode (Record {recordId = Proxy, recordValue = "value" })
{"recordValue":"value"}

Decodificar un valor sin ID

ghci> decode "{\"recordValue\":\"value\"}" :: Maybe RecordWithoutId
Just (Record {recordId = Proxy, recordValue = "value"})
Joe avatar Feb 17 '2024 04:02 Joe