如何在 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
是像 Int
或 Map 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
中的值为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
.
等较不抽象的
但是,这仍然是错误的,因为约束在错误的位置。你在这里所做的是这样说:如果你有一个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 m
和IsSpriteMap 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" 基于散列的容器,而无需修改签名或导入它的代码。
我想将键类型限制为 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
是像 Int
或 Map 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
中的值为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
.
但是,这仍然是错误的,因为约束在错误的位置。你在这里所做的是这样说:如果你有一个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 m
和IsSpriteMap 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" 基于散列的容器,而无需修改签名或导入它的代码。