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
的任何地方都取消了heave
case
语法:

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