代码之家  ›  专栏  ›  技术社区  ›  John F. Miller

解析不明确的类型变量

  •  2
  • John F. Miller  · 技术社区  · 6 年前

    我有两个功能:

    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' )

    load send 是多态的。不知何故编译器必须决定 (以及扩展什么版本的 toJSON )使用。

    是。这些信息实际上被编码在磁盘上的数据和 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}
    

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

    3 回复  |  直到 6 年前
        1
  •  6
  •   Ben    6 年前

    根据字符串中的内容,不能生成将字符串数据转换为不同类型的值的函数。这根本不可能。您需要重新排列内容,以便返回类型不依赖于字符串内容。

    你喜欢的类型 load Asset a => Reference -> IO (Maybe a) 说“随便选” a (其中 Asset a Reference ,我会还给你一个 IO Maybe a ". 调用者选择他们希望由引用加载的类型;文件的内容不影响加载的类型。但是你不希望它被调用者选择,你希望它被存储在磁盘上的内容选择,所以类型签名根本不表示你真正想要的操作。这是您真正的问题;在组合时类型变量不明确 负载 send TypeApplications )如果 负载 发送 它们各自都是正确的,合并是唯一的问题。

    负载

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

    data SomeAsset
      where Some :: Asset a => a -> SomeAsset
    
    load :: Reference -> IO (Maybe SomeAsset)
    

    负载 不再是多态的。你得到一个 SomeAsset Asset 实例。 负载 可以在内部使用它想要的任何逻辑拆分成多个分支,并在不同分支上产生不同类型资产的值;前提是每个分支以 一些资产 构造函数所有分支都将返回同一类型。

    它,你会用一些类似的东西(忽略我没有处理 Nothing ):

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

    一些资产 包装器保证 资产 保留其包装值,因此您可以打开它们并调用 资产 -结果的多态函数。但是,您永远不能以任何其他方式对依赖于特定类型的值执行任何操作 1个 ,所以你得把它包起来 case 案例 表达式产生的类型依赖于包含的类型(例如 case someAsset of SomeAsset a -> a

    另一种方法是 RankNTypes 并给予 负载

    load :: (forall a. Asset a => a -> r) -> Reference -> IO (Maybe r)
    

    在这里 负载 根本不返回表示加载的资产的值。它所做的是将多态函数作为参数;该函数在任何 并返回一个类型 r (由 负载 可以根据需要在内部分支,并在不同的分支中构造不同类型的资产。不同的资产类型都可以传递给处理程序,因此可以在每个分支中调用处理程序。

    一些资产 朗克泰普斯

    withSomeAsset :: (forall a. Asset a => a -> r) -> (SomeAsset -> r)
    withSomeAsset f (SomeAsset a) = f a
    

    这避免了将代码重新构造为连续传递样式,但消除了困难 案例 一些资产 :

    loadAndSend :: Reference -> IO ()
    loadAndSend ref
      = do Just asset <- load ref
           withSomeAsset send asset
    

    或者甚至加上:

    sendSome = withSomeAsset send
    

    参考 ,操作程序通过声明在构造引用时简单地将同一问题移到该位置来反对。如果引用包含表示它们引用的资产类型的数据,则我将 强烈地 建议采纳丹尼尔的建议,并使用本答案中描述的概念在参考构建层解决该问题。 拥有一个类型参数可以防止混淆对错误类型的资产的引用,如果您知道类型的话。

    如果用相同类型的引用和资产进行显著的处理,那么在Word代码中拥有类型参数可以捕获容易出错的错误。 即使


    1个 从技术上讲 资产 Typeable ,以便您可以对其进行特定类型的测试,然后返回这些类型。

        2
  •  5
  •   Daniel Wagner    6 年前

    当然,确保 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 ()
    

    参考 通过存在性装箱来键入它。

    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
    
        3
  •  0
  •   John F. Miller    6 年前

    Daniel Wagner Ben

    首先,根据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) 因此资源始终与引用的类型匹配。(Yay类型安全!)

    这仍然不能解决如何加载泛型引用的问题。对于这些情况,我使用Ben的下半部分答案编写了一些新代码。通过将资源包装在 SomeAsset

    {-# 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
    
    推荐文章