如何在Haskell类型签名中为Lens指定At(类似于地图的类型)的类型参数?

时间:2018-10-19 00:42:39

标签: haskell lens

我想通过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)
   |                            ^^^^^^^^^^^^^^^^

2 个答案:

答案 0 :(得分:2)

At m提供了at :: Index m -> Lens' m (Maybe (IxValue m))。请注意,Lens' m _意味着m是像IntMap 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) ~ Spritem 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 em的证明。

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 signaturesmixin 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实例用于键类型。但是您需要它允许以后“填充”基于散列的容器,而无需修改签名或导入它的代码。