解决模糊类型变量

Joh*_*ler 2 haskell parametric-polymorphism

我有这两个功能:

load :: Asset a => Reference -> IO (Maybe  a)
send :: Asset a => a -> IO ()
Run Code Online (Sandbox Code Playgroud)

Asset类看起来像这样:

class (Typeable a,ToJSON a, FromJSON a) => Asset a where
  ref :: a -> Reference
  ...
Run Code Online (Sandbox Code Playgroud)

第一个从磁盘读取资产,第二个将JSON表示传输到WebSocket.孤立地,它们工作正常,但是当我将它们组合起来时,编译器无法推断出具体类型a应该是什么.(Could not deduce (Asset a0) arising from a use of 'load')

这是有道理的,我没有给一个具体类型和两个loadsend多态.不知何故,编译器必须决定使用哪个版本send(以及扩展名为什么版本toJSON).

我可以在运行时确定具体类型a是什么.这些信息实际上是在磁盘上的数据和Reference类型中编码的,但我不确定在编译时是否正在运行类型检查器.

有没有办法在运行时传递正确的类型仍然保持类型检查器快乐?


附加信息

参考文献的定义

data Reference = Ref {
    assetType:: String
  , assetIndex :: Int
  } deriving (Eq, Ord, Show, Generic)
Run Code Online (Sandbox Code Playgroud)

通过解析来自WebSocket的请求来推导引用,其中Parser来自Parsec库.

reference :: Parser Reference
reference = do 
  t <-    string "User" 
       <|> string "Port" 
       <|> string "Model"
       <|> ...
  char '-'
  i <- int
  return Ref {assetType = t, assetIndex =i}
Run Code Online (Sandbox Code Playgroud)

如果我添加了一个类型参数,Reference我只需将问题推回到解析器中.我仍然需要在编译时将我不知道的字符串转换为类型以使其工作.

Ben*_*Ben 6

您不能创建一个函数将字符串数据转换为不同类型的值,具体取决于字符串中的内容.那根本不可能.您需要重新排列事物,以便返回类型不依赖于字符串内容.

你的类型load,Asset a => Reference -> IO (Maybe a)说"挑选你喜欢的任何a地方Asset a,给我一个Reference,然后我会给你一个IO产生的行动Maybe a".调用者选择他们希望由引用加载的类型; 文件的内容不会影响加载的类型.但是你不希望调用者选择它,你希望它是通过存储在磁盘上的内容来选择的,所以类型签名根本不能表达你真正想要的操作.那是你真正的问题; 组合时的模糊类型变量,load并且send很容易解决(使用类型签名或TypeApplications)如果load并且send单独正确并且将它们组合是唯一的问题.

基本上你不能只load返回一个多态类型,因为如果它,那么调用者会(必须)决定它返回什么类型.有两种方法可以避免这种情况或多或少的等价:返回一个存在的包装器,或者使用rank 2类型并添加一个多态处理函数(continuation)作为参数.

使用存在包装器(需要GADTs扩展),它看起来像这样:

data SomeAsset
  where Some :: Asset a => a -> SomeAsset

load :: Reference -> IO (Maybe SomeAsset)
Run Code Online (Sandbox Code Playgroud)

注意load不再是多态的.你得到一个SomeAsset(就类型检查而言)可以包含任何具有Asset实例的类型.load可以在内部使用它想要的任何逻辑分成多个分支,并在不同的分支上提出不同类型资产的值; 如果每个分支以结构化器结束资产值,则SomeAsset所有分支都将返回相同的类型.

为了send它,你会使用类似(忽略我没有处理Nothing):

loadAndSend :: Reference -> IO ()
loadAndSend ref
  = do Just someAsset <- load ref
       case someAsset
         of SomeAsset asset -> send asset
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

这里load根本不返回表示已加载资产的值.它的作用是将多态函数作为参数; 该函数适用于任何Asset并返回一个类型r(由load调用者选择),所以再次load可以在内部分支,但它想要并在不同的分支中构造不同类型的资产.不同的资产类型都可以传递给处理程序,因此可以在每个分支中调用处理程序.

我的偏好通常是使用该SomeAsset方法,但随后也使用RankNTypes和定义一个辅助函数,如:

withSomeAsset :: (forall a. Asset a => a -> r) -> (SomeAsset -> r)
withSomeAsset f (SomeAsset a) = f a
Run Code Online (Sandbox Code Playgroud)

这样可以避免将代码重构为延续传递样式,但是除了需要使用的地方之外,还可以在case任何地方删除升沉语法SomeAsset:

loadAndSend :: Reference -> IO ()
loadAndSend ref
  = do Just asset <- load ref
       withSomeAsset send asset
Run Code Online (Sandbox Code Playgroud)

或者甚至添加:

sendSome = withSomeAsset send
Run Code Online (Sandbox Code Playgroud)

Daniel Wagner建议添加类型参数Reference,OP反对这一参数,声明只是在构造引用时将相同的问题移动到.如果引用包含表示它们所引用的资产类型的数据,那么我强烈建议采用Daniel的建议,并使用本答案中描述的概念在参考构建级别解决该问题.Reference拥有一个类型参数可以防止在您知道类型的情况下混淆对错误类型资产的引用.

如果你这样做显著处理与同类型的引用和资产,然后在你的主力代码的类型参数可以赶上容易出错把它们组合起来,即使你平时存在的类型远在码外的水平.


1技术上你Asset暗示Typeable,所以你可以测试它的特定类型,然后返回它们.


Dan*_*ner 5

当然,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 ()
Run Code Online (Sandbox Code Playgroud)

如有必要,您仍然可以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
Run Code Online (Sandbox Code Playgroud)