在Haskell中建模领域数据

17

我正在使用Haskell设计一个比较大型的Web应用程序,这完全是出于教育和兴趣。

我首先要开始编写我的领域/值对象。其中一个例子是用户(User)。以下是我目前想到的内容:

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

我的应用程序会有大约20个类似于上面类型的类型。当我说“类似于上面类型”时,我的意思是它们将具有一个id、审计信息和一些特定于该类型的信息(例如在User中的电子邮件)。

我无法理解的是,每个字段(例如User.userEmail)都会创建一个新的函数fieldName :: Type -> FieldType。对于20种不同的类型,命名空间似乎会很快变得非常拥挤。此外,我不喜欢将我的用户ID字段命名为userId。我更愿意将其命名为id。这有没有办法避免呢?

也许我应该提一下,我来自命令式编程世界,所以这个FP东西对我来说还是很新的(但也很令人兴奋)。


您可以使用模块系统来控制名称范围和资格。 - Don Stewart
这是否意味着为每种类型创建一个模块,并执行“import qualified Model.User as User”(例如)? - three-cups
哦,只需使用 import qualified 来控制记录标签字段爆炸问题。 - Don Stewart
3
请注意,将id作为函数名是个不好的选择;在Prelude中已经定义了它表示恒等函数(id :: a -> a, id x = x)。 - Antal Spector-Zabusky
@Antal S-Z:我同意,但id通常用于持久化数据中的对象ID。在解决冲突时,我认为两种方法都是合理的。 - John L
1
@John,由于id在Haskell预定义中,我通常使用ident - luqui
3个回答

14

在Haskell中,命名空间可能有点麻烦。我通常会收紧我的抽象,直到没有那么多的名称为止,这也可以实现更多的重用。对于你的问题,我会创建一个数据类型而不是类来存储审核信息:

data Audit = Audit {
    creationDate :: Integer,
    lastUpdatedDate :: Integer,
    creationUser :: User,
    lastUpdatedUser :: User
}

然后将其与特定于类型的数据配对:

data User = User { 
    userAudit :: Audit,
    userId :: Integer,
    userEmail :: String
}

如果您想要,仍然可以使用那些类型类:

class Audited a where
    audit :: a -> Audit

class Identified a where
    ident :: a -> Integer

当你的设计不断发展时,要敞开心扉,接受那些typeclass可能会消失的可能性。像对象一样的typeclass——即每个方法都以类型a的单参数形式出现——有一种简化自身的方式。

另一种方法是使用参数化类型对对象进行分类:

data Object a = Object {
    objId    :: Integer,
    objAudit :: Audit,
    objData  :: a
}

看这里,Object 是一个 Functor

instance Functor Object where
    fmap f (Object id audit dta) = Object id audit (f dta)

基于我的设计直觉,我更倾向于这样做。没有更多关于你计划的信息,很难说哪种方式更好。看,那些类型类的需求已经消失了。


2
与Robin Green的评论一致,绝对要研究镜头(data-accessor或Hackage上的fclabels),以便处理记录的记录(记录...)。 - luqui

8
这是Haskell记录的已知问题。有一些建议(特别是TDNR)来缓解这种影响,但目前还没有解决方案。
如果您不介意将每个数据对象放在单独的模块中,则可以使用命名空间来区分函数:
import qualified Model.User as U
import qualified Model.Privileges as P

someUserId user = U.id user
somePrivId priv = P.id priv

关于使用id代替userId; 如果默认导入的id被隐藏了,这是可能的。请使用以下作为你的第一个导入语句:
import Prelude hiding (id)

现在通常的id函数将不在作用域内。如果你因某种原因需要它,可以使用完全限定名称来访问它,即Prelude.id

在创建可能与Prelude冲突的名称之前,请仔细考虑。这往往会使程序员感到困惑,并且使用起来有些尴尬。您最好使用简短的通用名称,例如oId


0
一个简单的选择是,不要全力以赴地使用类型类,而是将所有类型都变成单个代数数据类型的各种形式:
data DomainObject = User {
                      objectID :: Int,
                      objectCreationDate :: Date
                      ...
                    }
                  | SomethingElse {
                      objectID :: Int,
                      objectCreationDate :: Date,
                      somethingProperty :: Foo
                      ...
                    }
                  | AnotherThing {
                      objectID :: Int,
                      objectCreationDate :: Date,
                      anotherThingProperty :: Bar
                      ...
                    }

这个方法比较笨重,因为它要求所有的数据结构都在一个文件中,但至少它允许你使用同一个函数(objectID)来获取对象的ID。


2
因为这里的每个东西都有一个ID和创建日期,将这两个东西打包成一个通用数据类型是有意义的。我觉得镜头在这里可能会很有用。 - Robin Green
@Robin Green - 我在哪里可以找到使用镜头的示例?我找到了这个:http://hackage.haskell.org/package/lenses,但是没有示例。谢谢! - three-cups
算了,我找到了这个:http://hackage.haskell.org/packages/archive/lenses/0.1.4/doc/html/Data-Lenses.html - three-cups

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接