【问题标题】:How to elegantly avoid "Ambiguous type variable" when using Haskell type classes使用 Haskell 类型类时如何优雅地避免“模糊类型变量”
【发布时间】:2019-04-22 04:18:37
【问题描述】:

我想编写一个处理持久实体的简单框架。 这个想法是拥有一个实体类型类并提供通用的持久性操作,例如

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

【问题讨论】:

  • 多种方式;将其传递给仅适用于 Users 的函数,例如,或对其进行模式匹配。我认为这在实际使用中的问题要比在像这个唯一消费者也是多态的玩具示例中要小得多。
  • 感谢丹尼尔的快速回复。我同意在很多情况下消费者可能知道实际的数据类型。但我希望能够链接多态动作。在这种情况下,消费者也是多态的。有什么想法吗?
  • @ThomasMahler 你会考虑像retrieveEntity 1 &gt;&gt;= publishEntity @User 这样更好(使用TypeApplications 扩展名)吗?您无法避免需要指定所需类型somewhere 的事实。在此示例中,如果单态函数 publishEntity @User 要接收适当的参数,编译器可以推断 retrieveEntity 必须返回 IO User

标签: haskell typeclass parametric-polymorphism


【解决方案1】:

您可以通过将类型级别标签添加到实体标识符 Integer 来将 storeEntityretrieveEntity 的类型绑定在一起。我认为您的 API 设计也有一个不重要的小错误,但无论如何我都会修复它。即:Users 不应存储其标识符。而是为已识别的事物使用单个顶级类型包装器。这使您可以一劳永逸地编写所有带有标识符的代码——例如一个函数,它接受一个还没有 ID 的实体(你甚至如何用你的 User 的定义来表示它?)并为它分配一个新的 ID——无需返回并修改你的 Entity 类和它的所有实现。同样存储名字和姓氏的还有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 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,即使我们没有明确的类型签名。 (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,您将失去用于store 和@987654347 的标识符上的类型标签之间的联系@,然后回到你模棱两可的情况。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-07
    • 2012-12-03
    • 2019-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多