使用 Haskell 类型 类 时如何优雅地避免 "Ambiguous type variable"
How to elegantly avoid "Ambiguous type variable" when using Haskell type classes
我想编写一个处理持久化实体的简单框架。
这个想法是拥有一个实体类型 class 并提供像
这样的通用持久性操作
storeEntity :: (Entity a) => a -> IO ()
retrieveEntity :: (Entity a) => Integer -> IO a
publishEntity :: (Entity a) => a -> IO ()
实际数据类型是该实体类型的实例 class。
即使持久性操作是通用的并且不需要有关具体数据类型的任何信息您必须在调用站点提供类型注释以使 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
您可以将 storeEntity
和 retrieveEntity
的类型联系在一起,方法是向您的实体标识符 Integer
添加类型级标签。我认为您的 API 设计还有一个不严重的小缺陷,但无论如何我都会修复它。即:User
s 不应存储其标识符。取而代之的是为已识别的事物使用一个顶级类型包装器。这使您可以一劳永逸地编写代码来修改标识符——例如一个函数,它接受一个还没有 ID 的实体(你甚至如何用你对 User
的定义来表示它?)并为其分配一个新的 ID —— 无需返回并修改你的 Entity
class 及其所有实现。同样分别存储名字和姓氏的是 wrong。所以:
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 可能如下所示:
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
作为上述 "write it once and for all" 原则的一个示例,您可能会发现 retrieve
将实体 returns 与其标识符实际关联起来很方便。现在可以对所有实体统一执行此操作:
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
,即使我们没有明确的类型签名。 (storeRetrievePublish
上的签名也是可选的;这里给出的是 GHC 推断的签名。)收尾:
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
,您将失去用于 [ 的标识符上的类型标签之间的联系=34=] 和 retrieve
,然后回到你的模棱两可的类型情况。
我想编写一个处理持久化实体的简单框架。 这个想法是拥有一个实体类型 class 并提供像
这样的通用持久性操作storeEntity :: (Entity a) => a -> IO ()
retrieveEntity :: (Entity a) => Integer -> IO a
publishEntity :: (Entity a) => a -> IO ()
实际数据类型是该实体类型的实例 class。
即使持久性操作是通用的并且不需要有关具体数据类型的任何信息您必须在调用站点提供类型注释以使 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
您可以将 storeEntity
和 retrieveEntity
的类型联系在一起,方法是向您的实体标识符 Integer
添加类型级标签。我认为您的 API 设计还有一个不严重的小缺陷,但无论如何我都会修复它。即:User
s 不应存储其标识符。取而代之的是为已识别的事物使用一个顶级类型包装器。这使您可以一劳永逸地编写代码来修改标识符——例如一个函数,它接受一个还没有 ID 的实体(你甚至如何用你对 User
的定义来表示它?)并为其分配一个新的 ID —— 无需返回并修改你的 Entity
class 及其所有实现。同样分别存储名字和姓氏的是 wrong。所以:
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 可能如下所示:
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
作为上述 "write it once and for all" 原则的一个示例,您可能会发现 retrieve
将实体 returns 与其标识符实际关联起来很方便。现在可以对所有实体统一执行此操作:
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
,即使我们没有明确的类型签名。 (storeRetrievePublish
上的签名也是可选的;这里给出的是 GHC 推断的签名。)收尾:
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
,您将失去用于 [ 的标识符上的类型标签之间的联系=34=] 和 retrieve
,然后回到你的模棱两可的类型情况。