Can I type a partial record?
Asked Answered
B

3

10

I have a Person record with a name and an id, and a function createPerson that returns a Person without id, leaving generating the UUID to the caller.:

-- Person.hs
import Data.UUID (UUID)

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

createPerson name = Person { name = name }

Is there a way type a Person without an id, to inform the Caller that id Person will throw an exception? I've considered defining a PartialPerson as follows:

data PartialPerson = { name :: String }

but this quickly gets cumbersome when I want to add or change fields.

Baritone answered 17/2, 2021 at 12:24 Comment(2)
An easy type for a record that is missing its id would be UUID -> Person. The problem is that you can't print it or inspect its other fields.Titulary
Perhaps you could consider using a parametric type for this.Strained
T
10

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.

Titulary answered 17/2, 2021 at 13:14 Comment(3)
Relevant talk (2016) youtube.com/watch?v=BHjIl81HgfETitulary
One more option: data Person f = Person { name :: String, uuid :: f UUID }; type PersonWithId = Person Identity; type PersonWithoutId = Person (Const ()). It looks similar to your second option, but has some attractive generalizable operations.Dolt
Besides the "rank2classes" library hackage.haskell.org/package/rank2classes mentioned in the link, another library which supports that style of programming is "barbies" hackage.haskell.org/package/barbiesTitulary
K
5

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
Kinsfolk answered 17/2, 2021 at 12:50 Comment(3)
I'm not sure why you would define getId when id :: Person -> Maybe UUID already exists (ignoring the fact that id is probably not a good field name.)Answer
@Answer why do you say id is not a good field name?Baritone
Because id :: a -> a is already a function.Answer
S
2

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 }
Stepper answered 17/2, 2021 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.