我有以下两个功能:
load :: Asset a => Reference -> IO (Maybe a)
send :: Asset a => a -> IO ()
Asset类如下所示:
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
添加类型参数,我只是将问题推回到解析器中。我仍然需要将在编译时不知道的字符串转换为可以完成此工作的类型。
最佳答案
您无法创建根据字符串中的内容将字符串数据转换为不同类型的值的函数。那根本不可能。您需要重新安排事情,以便您的返回类型不依赖于字符串内容。
您为load
输入的类型,Asset a => Reference -> IO (Maybe a)
说“选择您喜欢的任何a
(其中为Asset a
)并给我一个Reference
,然后我会给您发回一个产生IO
的Maybe a
动作。 ”。调用者通过引用选择他们希望加载的类型;文件的内容不影响加载的类型。但是,您不希望它被调用方选择,而是希望它被存储在磁盘上的内容选择,因此类型签名根本无法表达您实际想要的操作。那是你真正的问题;如果load
和send
分别正确,并且将它们组合在一起是唯一的问题,则将TypeApplications
和load
组合时的歧义类型变量将很容易解决(带有类型签名或send
)。
基本上,您不能只让load
返回多态类型,因为如果这样做,则调用方必须(必须)确定其返回的类型。有两种避免这种情况的方法,它们或多或少是等效的:返回一个存在的包装器,或使用等级2类型并添加一个多态处理程序函数(continuation)作为参数。
使用现有的包装器(需要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
这样避免了将代码重组为延续传递样式的麻烦,但是在需要使用
case
的任何地方都取消了SomeAsset
语法:loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just asset <- load ref
withSomeAsset send asset
甚至添加:
sendSome = withSomeAsset send
Daniel Wagner建议将类型参数添加到
Reference
,OP表示反对,只是指出将相同的问题移至构造引用时。如果参考文献包含代表其所指资产类型的数据,那么我强烈建议您采纳Daniel的建议,并使用此答案中描述的概念在参考文献构建级别解决该问题。 Reference
具有类型参数可以防止混淆对您确实知道类型的资产的错误引用。而且,如果您对相同类型的引用和资产进行了大量处理,那么即使您通常在代码的外部层次上都存在类型,在主力代码中使用type参数也可以捕获将它们混在一起的简单错误。
1从技术上讲,您的
Asset
暗含Typeable
,因此您可以针对特定类型对其进行测试,然后将其返回。
关于haskell - 解决模糊类型变量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51831989/