解析不明确的类型变量
Posted
技术标签:
【中文标题】解析不明确的类型变量【英文标题】:Resolving an ambiguous type variable 【发布时间】:2018-08-13 23:53:59 【问题描述】:我有这两个功能:
load :: Asset a => Reference -> IO (Maybe a)
send :: Asset a => a -> IO ()
资产类如下所示:
class (Typeable a,ToJSON a, FromJSON a) => Asset a where
ref :: a -> Reference
...
第一个从磁盘读取资产,第二个将 JSON 表示传输到 WebSocket。孤立地它们工作正常,但是当我将它们组合起来时,编译器无法推断出a
应该是什么具体类型。 (Could not deduce (Asset a0) arising from a use of 'load'
)
这是有道理的,我没有给出具体的类型,load
和 send
都是多态的。不知何故,编译器必须决定使用哪个版本的send
(以及扩展的toJSON
的哪个版本)。
我可以在运行时确定a
的具体类型是什么。这些信息实际上是在磁盘上的数据和Reference
类型中编码的,但是我在编译时不确定,因为类型检查器正在运行。
有没有办法在运行时传递正确的类型,同时仍然让类型检查器满意?
其他信息
引用的定义
data Reference = Ref
assetType:: String
, assetIndex :: Int
deriving (Eq, Ord, Show, Generic)
引用是通过解析来自 WebSocket 的请求得到的,如下所示,其中 Parser 来自 Parsec 库。
reference :: Parser Reference
reference = do
t <- string "User"
<|> string "Port"
<|> string "Model"
<|> ...
char '-'
i <- int
return Ref assetType = t, assetIndex =i
如果我向Reference
添加了一个类型参数,我只是将我的问题推回到解析器中。我仍然需要将一个我在编译时不知道的字符串转换成一个类型来完成这项工作。
【问题讨论】:
【参考方案1】:您不能创建一个函数来根据字符串中的内容将字符串数据转换为不同类型的值。这简直是不可能的。您需要重新排列,以便您的返回类型不依赖于字符串内容。
load
、Asset a => Reference -> IO (Maybe a)
的类型表示“选择任何你喜欢的a
(其中Asset a
)并给我一个Reference
,我会给你一个IO
产生的操作Maybe a
"。调用者选择他们期望被引用加载的类型;文件的内容不影响加载的类型。但是你不希望它被调用者选择,你希望它被存储在磁盘上的东西选择,所以类型签名根本不表达你真正想要的操作。那是你真正的问题;如果 load
和 send
分别正确并且组合它们是唯一的问题,则组合 load
和 send
时的模棱两可的类型变量将很容易解决(使用类型签名或 TypeApplications
)。
基本上你不能只让load
返回一个多态类型,因为如果它返回,那么调用者就(必须)决定它返回什么类型。有两种方法可以避免这种情况,它们或多或少是等效的:返回一个存在包装器,或者使用 rank 2 类型并添加一个多态处理函数(延续)作为参数。
使用存在包装器(需要GADTs
扩展),它看起来像这样:
data SomeAsset
where Some :: Asset a => a -> SomeAsset
load :: Reference -> IO (Maybe SomeAsset)
注意load
不再是多态的。你会得到一个SomeAsset
,它(就类型检查器而言)可以包含任何具有Asset
实例的类型。 load
可以在内部使用它想要拆分为多个分支的任何逻辑,并在不同的分支上得出不同类型资产的值;如果每个分支都以使用 SomeAsset
构造函数包装资产值结束,那么所有分支都将返回相同的类型。
对于send
它,你会使用类似的东西(忽略我没有处理Nothing
):
loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just someAsset <- load ref
case someAsset
of SomeAsset asset -> send asset
SomeAsset
包装器保证 Asset
保留其包装值,因此您可以解开它们并在结果上调用任何 Asset
-polymorphic 函数。但是,您永远无法以任何其他方式对取决于特定类型的值进行任何操作1,这就是为什么您必须将其包裹起来并始终保持case
匹配的原因;如果case
表达式导致的类型取决于所包含的类型(例如case someAsset of SomeAsset a -> a
),则编译器将不会接受您的代码。
另一种方法是使用RankNTypes
并给load
一个这样的类型:
load :: (forall a. Asset a => a -> r) -> Reference -> IO (Maybe r)
这里load
根本不返回代表加载资产的值。它所做的是将多态函数作为参数;该函数适用于任何Asset
并返回一个类型r
(由load
的调用者选择),因此load
可以在内部分支但它想要并在不同的分支中构造不同类型的资产。不同的资产类型都可以传递给处理程序,因此处理程序可以在每个分支中调用。
我的偏好通常是使用SomeAsset
方法,但随后也使用RankNTypes
并定义一个辅助函数,例如:
withSomeAsset :: (forall a. Asset a => a -> r) -> (SomeAsset -> r)
withSomeAsset f (SomeAsset a) = f a
这避免了将您的代码重组为连续传递样式,但在您需要使用SomeAsset
的任何地方都消除了case
语法的繁重:
loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just asset <- load ref
withSomeAsset send asset
甚至添加:
sendSome = withSomeAsset send
Daniel Wagner 建议将类型参数添加到 Reference
,OP 表示反对,因为它只是将相同的问题移到构造引用时。如果引用包含表示它们引用的资产类型的数据,那么我强烈建议采纳 Daniel 的建议,并使用此答案中描述的概念在引用构建级别解决该问题。 Reference
具有类型参数可防止在您知道类型的情况下混淆对错误类型资产的引用。
如果你对相同类型的引用和资产进行大量处理,那么在你的主力代码中使用类型参数可以发现容易混淆它们的错误即使你通常存在于该类型之外代码的外层。
1 从技术上讲,您的Asset
隐含Typeable
,因此您可以针对特定类型对其进行测试,然后返回这些类型。
【讨论】:
【参考方案2】:当然,让Reference
存储类型。
data Reference a where
UserRef :: Int -> Reference User
PortRef :: Int -> Reference Port
ModelRef :: Int -> Reference Model
load :: Asset a => Reference a -> IO (Maybe a)
send :: Asset a => a -> IO ()
如有必要,您仍然可以通过存在性装箱来恢复原始Reference
类型的优点。
data SomeAsset f where SomeAsset :: Asset a => f a -> SomeAsset f
reference :: Parser (SomeAsset Reference)
reference = asum
[ string "User" *> go UserRef
, string "Port" *> go PortRef
, string "Model" *> go ModelRef
]
where
go :: Asset a => (Int -> Parser (Reference a)) -> Parser (SomeAsset Reference)
go constructor = constructor <$ char '-' <*> int
loadAndSend :: SomeAsset Reference -> IO ()
loadAndSend (SomeAsset reference) = load reference >>= traverse_ send
【讨论】:
好的,但现在我刚刚将我的问题从Asset
推到Reference
?我现在需要创建一个Reference a
,在我从WebSocket 或磁盘读取数据之前,我不知道a
是什么。我认为如果可以选择,我更愿意将此问题与 Asset 类隔离开来,在代码的 IO 部分之外,该类不太常见。
@JohnF.Miller 您在问题中说,“我可以在运行时确定a
的具体类型是什么。此信息以Reference
类型编码。”。所以你只需要修改Reference
,以编译器可以看到的方式携带这些信息。当然,我不能就如何完全盲目地做到这一点提供建议——您必须提供有关 如何 Reference
携带此信息的详细信息,以便获得比我所写的更有针对性的信息至今。编辑:我看到您已经编辑了问题以包括其中的一些内容。我会更新我的答案来讨论这个问题。
@JohnF.Miller 我已经包含了一些进一步的细节来模拟你的更新信息。【参考方案3】:
在查看了来自 Daniel Wagner 和 Ben 的答案后,我最终使用了两者的组合解决了我的问题,我将其放在这里,希望它能帮助其他人。
首先,根据 Daniel Wagner 的回答,我在 Reference
中添加了一个幻像类型:
data Reference a = Ref
assetType:: String
, assetIndex :: Int
deriving (Eq, Ord, Show, Generic)
我选择不使用 GADT 构造函数并将字符串引用保留为 assetType
,因为我经常通过网络发送引用和/或从传入的文本中解析它们。我觉得有太多代码点需要通用参考。对于这些情况,我用Void
填写幻像类型:
-# LANGUAGE EmptyDataDecls #-
data Void
-- make this reference Generic
voidRef :: Reference a -> Reference Void
castRef :: a -> Reference b -> Reference a
-- ^^^ Note this can be undefined used only for its type
这样load
类型签名变为load :: Asset a => Reference a -> IO (Maybe a)
所以资产总是匹配引用的类型。 (耶类型安全!)
这仍然没有解决如何加载通用引用。对于这些情况,我使用本回答的后半部分编写了一些新代码。通过将资产包装在 SomeAsset
中,我可以返回一个让类型检查器满意的类型。
-# LANGUAGE GADTs #-
import Data.Aeson (encode)
loadGenericAsset :: Reference Void -> IO SomeAsset
loadGenericAsset ref =
case assetType ref of
"User" -> Some <$> load (castRef (undefined :: User) ref)
"Port" -> Some <$> load (castRef (undefined :: Port) ref)
[etc...]
send :: SomeAsset -> IO ()
send (Some a) = writeToUser (encode a)
data SomeAsset where
Some :: Asset a => a -> SomeAsset
【讨论】:
以上是关于解析不明确的类型变量的主要内容,如果未能解决你的问题,请参考以下文章