Jamison Dance
Jamison Dance

Reputation: 20174

How do I create several related data types in haskell?

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

Answers (1)

Tikhon Jelvis
Tikhon Jelvis

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

Related Questions