I'm working on designing a larger-ish web application using Haskell. This is purely for my education and interest.
I'm starting by writing out my domain/value objects. One example is a User. Here's what I've come up with so far
module Model (User) where
class Audited a where
creationDate :: a -> Integer
lastUpdatedDate :: a -> Integer
creationUser :: a -> User
lastUpdatedUser :: a -> User
class Identified a where
id :: a -> Integer
data User = User { userId :: Integer
, userEmail :: String
, userCreationDate :: Integer
, userLastUpdatedDate :: Integer
, userCreationUser :: User
, userLastUpdatedUser :: User
}
instance Identified User where
id u = userId u
instance Audited User where
creationDate u = userCreationDate
lastUpdatedDate u = userLastUpdatedDate
creationUser u = userCreationUser
lastUpdatedUser u = userLastUpdatedUser
My application will have roughly 20 types like the above type. When I say "like the above type", I mean they will have an id, audit information, and some type-specific information (like email in the case of User).
The thing I can't wrap my mind around is the fact that each of my fields (e.g. User.userEmail) creates a new function fieldName :: Type -> FieldType
. With 20 different types, the namespace seems like it'll get pretty full pretty fast. Also, I don't like having to name my User ID field userId
. I'd rather name it id
. Is there any way around this?
Maybe I should mention that I'm coming from the imperative world, so this FP stuff is pretty new (yet pretty exciting) for me.
Yeah, namespacing can be kind of a pain in Haskell. I usually end up tightening up my abstractions until there are not so many names. It also allows for more reuse. For yours, I would make a data type rather than a class for the audit information:
data Audit = Audit {
creationDate :: Integer,
lastUpdatedDate :: Integer,
creationUser :: User,
lastUpdatedUser :: User
}
And then pair that up with the type-specific data:
data User = User {
userAudit :: Audit,
userId :: Integer,
userEmail :: String
}
You can still use those typeclasses if you want:
class Audited a where
audit :: a -> Audit
class Identified a where
ident :: a -> Integer
However as your design develops, be open to the possibility of those typeclasses dissolving into thin air. Object-like typeclasses -- ones where every method takes a single parameter of type a
-- have a way of simplifying themselves away.
Another way to approach this might be to classify your objects with a parametric type:
data Object a = Object {
objId :: Integer,
objAudit :: Audit,
objData :: a
}
Check it out, Object
is a Functor
!
instance Functor Object where
fmap f (Object id audit dta) = Object id audit (f dta)
I would be more inclined to do it this way, based on my design hunch. It is hard to say which way is better without knowing more about your plans. And look, the need for those typeclasses dissolved away. :-)
This is a known problem with Haskell's records. There have been some suggestions (notably TDNR) to mitigate the effects, but no solutions have emerged yet.
If you don't mind putting each of your data objects in a separate module, then you can use namespaces to differentiate between the functions:
import qualified Model.User as U
import qualified Model.Privileges as P
someUserId user = U.id user
somePrivId priv = P.id priv
As to using id
instead of userId
; it's possible if you hide the id
which is imported from the Prelude by default. Use the following as your first import statement:
import Prelude hiding (id)
and now the usual id
function won't be in scope. If you need it for some reason, you can access it with a fully-qualified name, i.e. Prelude.id
.
Think carefully before creating a name that clashes with the Prelude. It can often be confusing for the programmer, and it's slightly awkward to work with. You may be better off using short, generic name, such as oId
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With