Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modeling domain data in Haskell [closed]

Tags:

haskell

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.

like image 534
three-cups Avatar asked Apr 25 '11 04:04

three-cups


2 Answers

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. :-)

like image 137
luqui Avatar answered Oct 16 '22 05:10

luqui


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.

like image 21
John L Avatar answered Oct 16 '22 05:10

John L