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

如何在Haskell类型签名中从Lens为At(类映射类型)指定类型参数?,haskell,haskell-lens,Haskell,Haskell Lens,我希望约束键类型为ImageId,值类型为Sprite,同时通过。这可能吗?我似乎得到了一种不匹配,基于类型签名,我不知道如何解决它。我的例子是: data Game m e = Game { initial :: e, -- ... sprites :: (At m) => IO (m ImageId Sprite) } 我的错误: * Expected kind `* -> * -> *', but `m' has kind `*' * In

我希望约束键类型为
ImageId
,值类型为
Sprite
,同时通过。这可能吗?我似乎得到了一种不匹配,基于类型签名,我不知道如何解决它。我的例子是:

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(可能是(IxValue m))
。请注意,
Lens'm
意味着
m
是一个像
Int
Map ImageId Sprite
这样的具体类型,而不是像
Map
这样的类型构造函数。如果您想说
m ImageId Sprite
是“类似地图的”,那么您需要以下3个约束:

  • At(m ImageId Sprite)
    :提供
    At
    用于索引和更新
  • Index(m ImageId Sprite)~ImageId
    :用于索引
    m ImageId Sprite
    s的键是
    ImageId
    s
  • IxValue(m ImageId Sprite)~Sprite
    :a
    m ImageId Sprite
    中的值是
    Sprite
    s
您可以尝试在
游戏
中设置此约束(尽管它仍然是错误的):

请注意,我说的是
m ImageId Sprite
a bazillion times,但我没有将
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
您将无法将其用于过于抽象的
游戏
,但它正好适合作为
游戏SpriteMap e
的较不抽象的游戏

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

如果要将
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
}
要构建一个
游戏e
,您可以使用
iom
类型的任何东西来填充
精灵
,只要
ispritemap m
。当您在模式匹配中使用
Game e
时,模式匹配将绑定一个(未命名)类型变量(我们称之为
m
),然后它将为您提供
IO m
IsPritemap m
的证明

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

doSomething Game{..}=do sprites'我试图用and解决这个问题

首先,我在主库中输入以下“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)
我无法直接使用
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
未使用地图类型参数化。取而代之的是,整个库都有一个签名,该签名必须由库的用户稍后填写,从而使整个库变得不确定

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

{-# 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
可执行文件取决于主库和实现库。主库中的签名“洞”会自动填充,因为有一个同名的实现模块,并且它包含的声明满足签名

module Main where

import Game
import qualified Data.Map

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


此解决方案的一个缺点是,它需要一个键类型的
Hashable
实例,即使在示例中实现没有使用它。但是您需要它来允许以后“填充”基于散列的容器,而无需修改签名或导入它的代码。

答案非常好。最简单的选择是根本不使用参数,
sprites::Map ImageId IxValue
。这是一个非常清楚的答案,我学到了很多。在考虑了这两个建议之后,我有一个问题:为什么不选择所有的
?如果API的这一部分再次更改,那么只需要更改代码的一个方面(好吧,希望只有一个部分)。@bbarker有些事情你可以用一个来做,而用另一个来做则不行。如果您只想要它们共享的功能,那么在函数上添加上下文是最简单的解决方案(更少的语言扩展)。
module Main where

import Game
import qualified Data.Map

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