Haskell 解析不明确的类型变量
我有两个功能:Haskell 解析不明确的类型变量,haskell,parametric-polymorphism,Haskell,Parametric Polymorphism,我有两个功能: 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 ... data SomeAsset where Some :: Asset a => a -&
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
...
data SomeAsset
where Some :: Asset a => a -> SomeAsset
load :: Reference -> IO (Maybe SomeAsset)
load :: (forall a. Asset a => a -> r) -> Reference -> IO (Maybe r)
第一个从磁盘读取资产,第二个将JSON表示传输到WebSocket。孤立地说,它们工作得很好,但当我将它们组合在一起时,编译器无法推断出a
应该是什么具体类型。(无法推断(资产a0)是由使用“加载”引起的。
)
这是有道理的,我没有给出具体的类型,load
和send
都是多态的。不知何故,编译器必须决定使用哪个版本的发送
(以及扩展到哪个版本的toJSON
)
我可以在运行时确定a
的具体类型。这个信息实际上是在磁盘上的数据和引用
类型中编码的,但是我不确定在编译时是否运行了类型检查器
有没有一种方法可以在运行时传递正确的类型,并且仍然让类型检查器满意
其他信息 指称的定义
data Reference = Ref {
assetType:: String
, assetIndex :: Int
} deriving (Eq, Ord, Show, Generic)
引用是通过解析来自WebSocket的请求派生的,如下所示,其中解析器来自Parsec库
reference :: Parser Reference
reference = do
t <- string "User"
<|> string "Port"
<|> string "Model"
<|> ...
char '-'
i <- int
return Ref {assetType = t, assetIndex =i}
reference::解析器引用
参考=do
t当然,确保参考
存储类型
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 ()
如有必要,您仍然可以通过存在装箱来恢复原始引用
类型的优点
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
data SomeAsset f其中SomeAsset::Asset a=>fa->SomeAsset f
reference::Parser(SomeAsset引用)
参考=asum
[字符串“User”*>转到UserRef
,字符串“Port”*>转到PortRef
,字符串“Model”*>转到ModelRef
]
哪里
go::Asset a=>(Int->Parser(Reference a))->Parser(SomeAsset Reference)
go构造函数=构造函数IO()
loadAndSend(SomeAsset引用)=加载引用>>=遍历发送
您不能根据字符串中的内容将字符串数据转换为不同类型的值。那根本不可能。您需要重新安排内容,以便返回类型不依赖于字符串内容
您输入的加载
,资产a=>引用->IO(可能是a)
表示“选择您喜欢的资产a
(其中资产a
)并给我一个引用
,我会给您一个生成可能是a
的IO操作”。调用者选择他们期望由引用加载的类型;文件的内容不影响加载的类型。但您不希望调用者选择它,而是希望它由存储在磁盘上的内容选择,因此类型签名并不表示您实际需要的操作。这才是你真正的问题;如果load
和send
单独正确且组合它们是唯一的问题,则在组合load
和send
时,不明确的类型变量很容易解决(使用类型签名或TypeApplications
)
基本上,您不能让load
返回多态类型,因为如果它返回多态类型,那么调用者将(必须)决定它返回的类型。有两种方法可以或多或少地避免这种情况:返回一个存在包装器,或者使用秩2类型并添加一个多态处理函数(continuation)作为参数
使用存在包装(需要GADTs
扩展),它看起来像这样:
class (Typeable a,ToJSON a, FromJSON a) => Asset a where
ref :: a -> Reference
...
data SomeAsset
where Some :: Asset a => a -> SomeAsset
load :: Reference -> IO (Maybe SomeAsset)
load :: (forall a. Asset a => a -> r) -> Reference -> IO (Maybe r)
请注意,load
不再是多态的。您会得到一个SomeAsset
,它(就类型检查器而言)可以包含任何具有Asset
实例的类型load
可以在内部使用它想要拆分为多个分支的任何逻辑,并在不同分支上得出不同类型资产的值;如果每个分支都以使用SomeAsset
构造函数包装资产值结束,则所有分支都将返回相同的类型
要发送它,您可以使用如下内容(忽略我没有处理任何内容):
此处load
根本不返回表示已加载资产的值。它所做的是将多态函数作为参数;该函数作用于任何资产
,并返回一个类型r
(由load
的调用者选择),因此load
可以根据需要在内部进行分支,并在不同的分支中构造不同类型的资产。不同的资产类型都可以传递给处理程序,因此可以在每个分支中调用处理程序
我的偏好通常是使用SomeAsset
方法,但也可以使用RankNTypes
并定义一个助手函数,如:
withSomeAsset :: (forall a. Asset a => a -> r) -> (SomeAsset -> r)
withSomeAsset f (SomeAsset a) = f a
这避免了必须将代码重新构造为连续传递样式,但在需要使用SomeAsset
的任何地方都取消了heavecase
语法:
loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just asset <- load ref
withSomeAsset send asset
Daniel Wagner建议将类型参数添加到Reference
,OP对此表示反对,因为它指出,在构造引用时,同样的问题会转移到。如果引用包含代表其引用的资产类型的数据,那么我强烈建议采纳Daniel的建议,并使用本答案中描述的概念在引用构造级别解决该问题<代码>引用
具有类型参数可防止混淆对您知道类型的错误资产类型的引用
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 ()
如果您对同一类型的引用和资产进行了重要的处理,那么在您的主力代码中使用类型参数可能会发现容易混淆它们的错误,即使您通常将类型存在于代码的外部级别
1从技术上讲,您的资产
意味着可键入
,因此您可以对其进行特定类型的测试,然后返回THO
{-# 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