如何在 Haskell 类型签名中为来自 Lens 的 At(类映射类型)指定类型参数?

How to specify type parameters for At (map-like types) from Lens in Haskell type signature?

我想将键类型限制为 ImageId,将值类型限制为 Sprite,同时不限制地图的具体类型,方法是 At typeclass.这可能吗?我似乎有点不匹配,并且根据类型签名,我不知道如何解决它。我的例子:

data Game m e = Game {
  initial :: e,
  -- ...
  sprites :: (At m) => IO (m ImageId Sprite)
}

我的错误:

    * Expected kind `* -> * -> *', but `m' has kind `*'
    * In the first argument of `IO', namely `(m ImageId Sprite)'
      In the type `(At m) => IO (m ImageId Sprite)'
      In the definition of data constructor `Game'
   |
64 |   sprites :: (At m) => IO (m ImageId Sprite)
   |                            ^^^^^^^^^^^^^^^^

At m 提供 at :: Index m -> Lens' m (Maybe (IxValue m))。请注意,Lens' m _ 表示 m 是像 IntMap ImageId Sprite 这样的具体类型,而不是像 Map 这样的类型构造函数。如果你想说 m ImageId Sprite 是 "map-like",那么你需要这 3 个约束:

  • At (m ImageId Sprite): 提供 at 用于索引和更新。
  • Index (m ImageId Sprite) ~ ImageId:用于索引m ImageId Sprite的键是ImageId
  • IxValue (m ImageId Sprite) ~ Sprite: m ImageId Sprite中的值为Sprites.

您可以尝试将此约束放在 Game 中(尽管它仍然是错误的):

data Game m e = Game {
  initial :: e,
  -- ...
  sprites :: (At (m ImageId Sprite),
              Index (m ImageId Sprite) ~ ImageId,
              IxValue (m ImageId Sprite) ~ Sprite) =>
             IO (m ImageId Sprite)
}

请注意,我说 m ImageId Sprite 无数次,但我没有将 m 应用于其他(或更少)参数。这是一个线索,表明您实际上不需要对 m :: * -> * -> *(例如 Map 之类的东西)进行抽象。你只需要抽象超过 m :: *.

-- still wrong, though
type IsSpriteMap m = (At m, Index m ~ ImageId, IxValue m ~ Sprite)
data Game m e = Game {
  initial :: e,
  -- ...
  sprites :: IsSpriteMap m => IO m
}

这很好:如果您曾为此数据结构制作专门的映射,例如

data SpriteMap
instance At SpriteMap
type instance Index SpriteMap = ImageId
type instance IxValue SpriteMap = IxValue

您将无法将它与太抽象的 Game 一起使用,但它恰好适合 Game SpriteMap e.

等较不抽象的

但是,这仍然是错误的,因为约束在错误的位置。你在这里所做的是这样说:如果有一个Game m e,你可以得到一个m如果 证明 m 是映射的。如果我想 创建 一个 Game m e,我根本没有义务证明 m 是 mappish。如果您不明白为什么,想象一下您是否可以将 => 替换为上面的 -> 调用 sprites 的人传递的证明 m 就像一张地图,但 Game 本身不包含证明。

如果你想保留 m 作为 Game 的参数,你应该简单地写:

data Game m e = Game {
  initial :: e,
  -- ...
  sprites :: IO m
}

然后将每个需要使用 m 的函数写成像这样的映射:

doSomething :: IsSpriteMap m => Game m e -> IO ()

或者,您可以使用存在量化:

data Game e = forall m. IsSpriteMap m => Game {
  initial :: e,
  -- ...
  sprites :: IO m
}

要构造一个 Game e,您可以使用 IO m 类型的任何东西来填充 sprites,只要 IsSpriteMap m。当你在模式匹配中使用 Game e 时,模式匹配将绑定一个(未命名的)类型变量(我们称之为 m),然后它会给你一个 IO mIsSpriteMap m.

的证明
doSomething :: Game e -> IO ()
doSomething Game{..} = do sprites' <- sprites
                          imageId <- _
                          let sprite = sprites'^.at imageId
                          _

您还可以将 m 作为 Game 的参数,但仍将上下文保留在 Game 构造函数中。但是,我强烈建议您选择第一个选项,即为每个函数添加上下文,除非您有理由不这样做。

(此答案中的所有代码都会产生有关语言扩展的错误。请将它们粘贴在文件顶部的 {-# LANGUAGE <exts> #-} 编译指示中,直到安抚 GHC。)

我尝试使用 module signatures and mixin modules 来解决这个问题。

首先我在主库declared下面"Mappy.hsig"签名:

{-# language KindSignatures #-}
{-# language RankNTypes #-}
signature Mappy where

import Control.Lens
import Data.Hashable

data Mappy :: * -> * -> *

at' :: (Eq i, Ord i, Hashable i) => i -> Lens' (Mappy i v) (Maybe v)

由于 this limitation.

,我无法直接使用 At 类型类

然后我让库代码导入抽象签名而不是具体类型:

{-# language DeriveGeneric #-}
{-# language DeriveAnyClass #-}
module Game where

import Data.Hashable
import GHC.Generics
import Mappy (Mappy,at')

data ImageId = ImageId deriving (Eq,Ord,Generic,Hashable)

data Sprite = Sprite

data Game e = Game {
  initial :: e,
  sprites :: IO (Mappy ImageId Sprite)
}

库中的代码不知道 Mappy 的具体类型是什么,但它知道当键满足约束时 at' 函数可用。请注意,Game 未使用地图类型进行参数化。相反,整个图书馆 是无限期的,因为签名必须由图书馆的用户稍后填写。

在一个internal convenience library(或一个完全独立的包)中我定义了一个与签名同名的实现模块:

{-# language RankNTypes #-}
module Mappy where

import Data.Map.Strict
import Control.Lens
import Data.Hashable

type Mappy = Map 

at' :: (Eq i, Ord i, Hashable i) => i -> Lens' (Mappy i v) (Maybe v)
at' = at

可执行文件同时依赖于主库和实现库。主库中的签名"hole"是自动填充的,因为有一个同名的实现模块,并且它包含的声明满足签名。

module Main where

import Game
import qualified Data.Map

game :: Game () 
game = Game () (pure Data.Map.empty)

此解决方案的一个缺点是它需要一个 Hashable 键类型实例,即使如示例中所示,实现并未使用它。但是您以后需要它允许 "filling in" 基于散列的容器,而无需修改签名或导入它的代码。