我想编写一个处理持久实体的简单框架。 想法是拥有一个Entity类型类,并提供通用的持久性操作,如
storeEntity :: (Entity a) => a -> IO ()
retrieveEntity :: (Entity a) => Integer -> IO a
publishEntity :: (Entity a) => a -> IO ()
实际数据类型是该实体类型类的实例。
即使持久性操作是通用的并且不需要有关具体数据类型的任何信息,您也必须在调用站点提供类型注释以使GHC满意,例如:
main = do
let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
storeEntity user1
user2 <- retrieveEntity 1 :: IO User -- how to avoid this type annotation?
publishEntity user2
有什么方法可以避免这种呼叫站点注释?
我知道,如果编译器可以从用法的上下文中推断出实际类型,则不需要这些注释。因此,例如,以下代码可以正常工作:
main = do
let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
storeEntity user1
user2 <- retrieveEntity 1
if user1 == user2
then publishEntity user2
else fail "retrieve of data failed"
但是我希望能够像这样链接多态动作:
main = do
let user1 = User 1 "Heinz" "Meier" "hm@meier.com"
storeEntity user1
-- unfortunately the next line does not compile
retrieveEntity 1 >>= publishEntity
-- but with a type annotation it works:
(retrieveEntity 1 :: IO User) >>= publishEntity
但是这里使用类型注释会破坏多态性的优雅……
为完整起见,我提供了完整的源代码:
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module Example where
import GHC.Generics
import Data.Aeson
-- | Entity type class
class (ToJSON e, FromJSON e, Eq e, Show e) => Entity e where
getId :: e -> Integer
-- | a user entity
data User = User {
userId :: Integer
, firstName :: String
, lastName :: String
, email :: String
} deriving (Show, Eq, Generic, ToJSON, FromJSON)
instance Entity User where
getId = userId
-- | load persistent entity of type a and identified by id
retrieveEntity :: (Entity a) => Integer -> IO a
retrieveEntity id = do
-- compute file path based on id
let jsonFileName = getPath id
-- parse entity from JSON file
eitherEntity <- eitherDecodeFileStrict jsonFileName
case eitherEntity of
Left msg -> fail msg
Right e -> return e
-- | store persistent entity of type a to a json file
storeEntity :: (Entity a) => a -> IO ()
storeEntity entity = do
-- compute file path based on entity id
let jsonFileName = getPath (getId entity)
-- serialize entity as JSON and write to file
encodeFile jsonFileName entity
-- | compute path of data file based on id
getPath :: Integer -> String
getPath id = ".stack-work/" ++ show id ++ ".json"
publishEntity :: (Entity a) => a -> IO ()
publishEntity = print
main = do
let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
storeEntity user1
user2 <- retrieveEntity 1 :: IO User
print user2
答案 0 :(得分:4)
通过将类型级别的标签添加到实体的标识符storeEntity
,可以将retrieveEntity
和Integer
的类型绑定在一起。我认为您的API设计也不太重要,但是无论如何我都会进行修复。即:User
不应该存储其标识符。而是使用单个顶级类型包装器来标识已确定的内容。这使您可以一劳永逸地编写代码-例如一个函数,该函数采用一个尚没有ID的实体(您甚至将如何用User
的定义来表示它?)并为其分配一个新的ID,而无需返回并修改{{1 }}类及其所有实现。 wrong也分别存储名字和姓氏。所以:
Entity
我的import Data.Tagged
data User = User
{ name :: String
, email :: String
} deriving (Eq, Ord, Read, Show)
type Identifier a = Tagged a Integer
data Identified a = Identified
{ ident :: Identifier a
, val :: a
} deriving (Eq, Ord, Read, Show)
与您的Identified User
相对应,而我的User
在您的版本中没有类似物。 User
类可能看起来像这样:
Entity
作为上述“一劳永逸”原则的示例,您可能会发现class Entity a where
store :: Identified a -> IO ()
retrieve :: Identifier a -> IO a
publish :: a -> IO () -- or maybe Identified a -> IO ()?
instance Entity User -- stub
实际将返回的实体与其标识符相关联很方便。现在可以对所有实体统一进行此操作:
retrieve
现在,我们可以编写一个将其存储类型关联在一起的动作并检索动作:
retrieveIDd :: Entity a => Identifier a -> IO (Identified a)
retrieveIDd id = Identified id <$> retrieve id
这里storeRetrievePublish :: Entity a => Identified a -> IO ()
storeRetrievePublish e = do
store e
e' <- retrieve (ident e)
publish e'
具有足够丰富的类型信息,即使我们没有显式的类型签名,我们也知道ident e
必须是e'
。 (a
上的签名也是可选的;此处给出的签名是GHC推断出的签名。)结束语:
storeRetrievePublish
如果您不想显式定义main :: IO ()
main = storeRetrievePublish (Identified 1 (User "Thomas Meier" "tm@meier.com"))
,则可以避免:
storeRetrievePublish
...但是您无法进一步展开定义:如果将main :: IO ()
main = do
let user = Identified 1 (User "Thomas Meier" "tm@meier.com")
store user
user' <- retrieve (ident user)
publish user'
简化为ident user
,您将失去{{1 }}和1
,并返回到您的含糊类型情况。