使用Haskell类型类时如何优雅地避免“歧义类型变量”

时间:2018-11-19 18:44:28

标签: haskell typeclass parametric-polymorphism

我想编写一个处理持久实体的简单框架。 想法是拥有一个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

1 个答案:

答案 0 :(得分:4)

通过将类型级别的标签添加到实体的标识符storeEntity,可以将retrieveEntityInteger的类型绑定在一起。我认为您的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,并返回到您的含糊类型情况。