解决模糊类型变量

时间:2018-08-13 23:54:00

标签: 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
  ...

第一个从磁盘读取资产,第二个将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)

引用是通过解析来自WebSocket的请求而得出的,解析器来自Parsec库,如下所示。

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

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

3 个答案:

答案 0 :(得分:6)

您不能创建根据字符串中的内容将字符串数据转换为不同类型的值的函数。那根本不可能。您需要重新安排事情,以便您的返回类型不依赖于字符串内容。

您输入的loadAsset a => Reference -> IO (Maybe a)类型为“选择您喜欢的任何a(其中Asset a)并给我Reference,我会还给您一个产生IO的{​​{1}}动作”。调用者通过引用选择他们希望加载的类型。文件的内容不影响加载的类型。但是您不希望它被调用者选择,而是希望它被存储在磁盘上的东西选择,因此类型签名根本无法表达您实际想要的操作。那是你真正的问题;如果将Maybe aload分别正确并合并,则将sendTypeApplications合并时的歧义类型变量很容易解决(使用类型签名或load)。他们是唯一的问题。

基本上,您不能仅仅让send返回一个多态类型,因为如果这样做,则调用者必须(必须)决定它返回哪种类型。有两种避免这种情况的方法,它们或多或少是等效的:返回一个存在的包装器,或使用等级2类型并添加一个多态处理函数(连续)作为参数。

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

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多态函数。但是,您永远无法以任何其他方式对依赖于特定类型的值进行任何操作 1 ,这就是为什么您必须始终将其包装并且Asset始终与之匹配的原因;如果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

Daniel Wagner建议将类型参数添加到sendSome = withSomeAsset send 中,OP表示反对,因为它指出在构造引用时将相同的问题简单地移到了该位置。如果参考文献包含代表其所指资产类型的数据,那么我将强烈建议采用Daniel的建议,并使用此答案中描述的概念在参考文献构建级别解决该问题。 Reference使用类型参数可以防止混淆您确实知道类型的资产的错误类型。

如果您使用相同类型的引用和资产进行大量处理,那么在主力代码中使用type参数可以捕获将它们混在一起的简单错误,即使您通常将类型存续外层代码。


1 从技术上讲,您的Reference隐含着Asset,因此您可以测试它的特定类型,然后返回它们。

答案 1 :(得分: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 ()

如有必要,您仍然可以通过现有的装箱方式恢复原始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

答案 2 :(得分:0)

在回顾了Daniel WagnerBen的答案之后,我最终将两者结合起来解决了我的问题,希望对其他人有帮助。

首先,根据丹尼尔·瓦格纳(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中,我可以返回一个使类型检查器满意的Type。

{-# 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