Reputation: 20174
I have a User
type that represents a user saved in the database. However, when displaying users, I only want to return a subset of these fields so I made a different type without the hash
. When creating a user, a password
will be provided instead of a hash
, so I made another type for that.
This is clearly the worst, because there is tons of duplication between my types. Is there a better way to create several related types that all share some fields, but add some fields and remove others?
{-# LANGUAGE DeriveGeneric #}
data User = User {
id :: String,
email :: String,
hash :: String,
institutionId :: String
} deriving (Show, Generic)
data UserPrintable = UserPrintable {
email :: String,
id :: String,
institutionId :: String
} deriving (Generic)
data UserCreatable = UserCreatable {
email :: String,
hash :: String,
institutionId :: String
} deriving (Generic)
data UserFromRequest = UserFromRequest {
email :: String,
institutionId :: String,
password :: String
} deriving (Generic)
-- UGHHHHHHHHHHH
Upvotes: 3
Views: 109
Reputation: 68152
In this case, I think you can replace your various User
types with functions. So instead of UserFromRequest
, have:
userFromRequest :: Email -> InstitutionId -> String -> User
Note how you can also make separate types for Email
and InstitutionId
, which will help you avoid a bunch of annoying mistakes. This serves the same purpose as taking a record with labelled fields as an argument, while also adding a bit of extra static safety. You can just implement these as newtypes:
newtype Email = Email String deriving (Show, Eq)
Similarly, we can replace UserPrintable
with showUser
.
UserCreatable
might be a bit awkard however, depending on how you need to use it. If all you ever do with it is take it as an argument and create a database row, then you can refactor it into a function the same way. But if you actually need the type for a bunch of things, this isn't a good solution.
In this second case, you have a couple of decent options. One would be to just make id
a Maybe
and check it each time. A better one would be to create a generic type WithId a
which just adds an id
field to anything:
data WithId a = { id :: DatabaseId, content :: a }
Then have a User
type with no id
and have your database functions work with a WithId User
.
Upvotes: 2