我想通过At typeclass将键类型约束为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)
| ^^^^^^^^^^^^^^^^
答案 0 :(得分:2)
At m
提供了at :: Index m -> Lens' m (Maybe (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
的键是ImageId
。IxValue (m ImageId Sprite) ~ Sprite
:m ImageId Sprite
中的值为Sprite
s。您可以尝试将此约束放在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
。
但是,这仍然是错误的,因为约束放置在错误的位置。您在这里所做的是这样说的:如果 you 有一个Game m e
,并且如果 you 证明{{1 }}是mappish。如果我想创建一个m
,我没有义务证明m
完全是mappish。如果您不明白为什么,请想象是否可以将Game m e
替换为上面的m
。 呼叫 =>
的人正在传递证明->
就像地图的证明,但是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 m
您还可以将IsSpriteMap m
保留为doSomething :: Game e -> IO ()
doSomething Game{..} = do sprites' <- sprites
imageId <- _
let sprite = sprites'^.at imageId
_
的参数,但仍将上下文保留在m
构造函数中。但是,我敦促您只选择在每个函数上放置上下文的第一个选项,除非您有理由不这样做。
(此答案中的所有代码都会产生有关语言扩展的错误。请始终将其粘贴在文件顶部的Game
杂注中,直到放置GHC。)
答案 1 :(得分:0)
我尝试使用module signatures和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
可执行文件同时依赖于主库和实现库。主库中的签名“孔”会自动填充,因为存在一个具有相同名称的实现模块,并且其中包含的声明都满足该签名。
module Main where
import Game
import qualified Data.Map
game :: Game ()
game = Game () (pure Data.Map.empty)
此解决方案的一个缺点是,即使示例中未实现,它也需要一个Hashable
实例用于键类型。但是您需要它允许以后“填充”基于散列的容器,而无需修改签名或导入它的代码。