Can I type a partial record?

First way:

You can define Person with Maybe UUID:

data Person = Person { name :: String, id :: Maybe UUID }

Now you can define some useful functions:

partialPerson :: String -> Person
partialPerson n = Person { name = n, id = Nothing }

getId :: Person -> UUID
getId (Person _ (Just uuid)) = uuid
getId _ = error "person hasn't UUID"

But getId is unsafe function, so you must be careful when you use it.

Second way:

You can define new data-type:

data PartialPerson = PartialPerson { name :: String }

... and define such functions for converting between this types:

withId :: PartialPerson -> UUID -> Person
withId (PartialPerson n) uuid = Person n uuid

withoutId :: Person -> PartialPerson
withoutId (Person n _) = PartialPerson n

One possible type for persons without id would be:

type PersonWithoutId = UUID -> Person

The problem is that we can't print values of this type, or inspect their other fields, because functions are opaque.


Another option is to parameterize the type:

data Person a = Person { name :: String, personId :: a }
type PersonWithId = Person UUID
type PersonWithoutId = Person ()

The nice thing about this is that you still can easily derive useful typeclasses.


A third option is to remove the id from the person and just use a pair when needed:

type PersonWithId = (UUID,Person)

or with a dedicated type:

data WithId a = WithId { theId :: UUID, theValue :: a } deriving Functor

The problem with this third option is that it becomes more cumbersome to have functions that work with both varieties of persons.

Also, auto-derived FromJSON and ToJSON instances will possibly have undesired nesting.

And it's not very extensible when there's more than one optional property.


A variation on the idea of making Person parametric to support both persons with- and without id, whilst however keeping it rigid that the id field is about UUIDs, not just some arbitrary parameter type:

{-# LANGUAGE DataKinds, KindSignatures, GADTs #-}

data IdRequirement
  = NoIdNeeded | IdOptional | IdRequired

data IdField (reqId :: IdRequirement) where
  IdLess :: IdField 'NoIdNeeded
  IdOmitted :: IdField 'IdOptional
  IdProvided :: UUID -> IdField 'IdOptional
  RequiredId ::  UUID -> IdField 'IdRequired

data Person (reqId :: IdRequirement)
  = Person { personName :: String
           , personId :: IdField reqId }