我想知道如何在不使用deriving Show
的情况下为以下代码片段类型系列文章中介绍Gmap
数据类型实现instance Show
。
class GMapKey k where
data GMap k :: * -> *
empty :: GMap k v
lookup :: k -> GMap k v -> Maybe v
insert :: k -> v -> GMap k v -> GMap k v
instance GMapKey Int where
data GMap Int v = GMapInt (Data.IntMap.IntMap v)
empty = GMapInt Data.IntMap.empty
lookup k (GMapInt m) = Data.IntMap.lookup k m
insert k v (GMapInt m) = GMapInt (Data.IntMap.insert k v m)
对于此启动实现:
instance Show (GMap k v) where
show (GMapInt _) = undefined
编译器抛出:
* Couldn't match type `k' with `Int'
`k' is a rigid type variable bound by
the instance declaration
at /home/x/src/GMapAssoc.hs:27:10
Expected type: GMap k v
Actual type: GMap Int v
* In the pattern: GMapInt _
In an equation for `show': show (GMapInt _) = undefined
In the instance declaration for `Show (GMap k v)'
* Relevant bindings include
show :: GMap k v -> String
除了主要问题之外,我还想了解为什么编译器在这种情况下不抱怨:
instance Show (GMap k v) where
show _ = undefined
在不知道GMap
这样的数据族的数据构造函数上,您就无法对其进行模式匹配,而不知道它的键类型 - 即它不像 GADT,因为它是开放的,因此不可能涵盖所有情况。 因此,如果要实现通用显示,则需要在不直接访问表示的情况下执行此操作。 我有两个选择给你:
捕获全部实例
在稍微尝试了一下之后,我能想到的最简单方法是向类添加一个方法以在实例中使用。
class GMapKey k where
data GMap k :: * -> *
empty :: GMap k v
lookup :: k -> GMap k v -> Maybe v
insert :: k -> v -> GMap k v -> GMap k v
showGMap :: (Show v) => GMap k v -> String
然后,您可以创建一个通用的包罗万象实例。
instance (GMapKey k, Show v) => Show (GMap k v) where
show = showGMap
这比我想要的要少一点,但也不是太糟糕。 我对这种方法的主要抱怨是它排除了实例deriving Show
,即
instance GMapKey Int where
data GMap Int v = GMapInt (Data.IntMap.IntMap v)
deriving (Show)
...
是非法的,因为它与我们的包罗万象实例重叠。如果类型变得复杂,这可能会有点痛苦。 下一个方法不会遇到此问题。
无论如何,现在我们有一个实例,可以像往常一样使用它。
example :: (GMapKey k, Show v) => GMap k v -> String
example gmap = show gmap
字典方法
如果您需要使用deriving Show
,您可以使用constraints
包以更现代的方式执行此操作。
{-# LANGUAGE ScopedTypeVariables, TypeApplications #-}
import Data.Constraint
它还涉及添加一个方法,但它不是返回一个String
,而是返回一个完整的Show
字典。
class GMapKey k where
data GMap k :: * -> *
empty :: GMap k v
lookup :: k -> GMap k v -> Maybe v
insert :: k -> v -> GMap k v -> GMap k v
showGMap :: (Show v) => Dict (Show (GMap k v))
您可以使用deriving
实例化,并且showGMap
实例始终相同。
instance GMapKey Int where
data GMap Int v = GMapInt (Data.IntMap.IntMap v)
deriving (Show)
...
showGMap = Dict
(您甚至可以使用DefaultSignatures
来避免在实例中提及showGMap
!
不幸的是,一个包罗万象的实例仍然会与此重叠,因此我们不会有一个全局Show
实例来GMap
。 但是,我们可以在需要的任何地方使用withDict
example :: forall k v. (GMapKey k, Show v) => GMap k v -> String
example gmap = withDict @k @v (show gmap)
这以不同的方式令人讨厌。 幸运的是,我们只需要在需要一个通用的Show (GMap k v)
实例时才需要这样做——如果我们已经知道k
是什么,那么我们派生的特定Show
实例就可以工作了。
也许有一种方法可以两全其美?
GMapInt
构造函数特定于,嗯,GMap Int
,所以你不能用它来构造/解构GMap k
k
除了Int
。
您可能需要此实例:
instance Show (GMap Int v) where
show (GMapInt _) = undefined
或者,如果Int
是唯一一个你希望显示其地图的关键类型(我觉得很奇怪),
instance (k ~ Int) => Show (GMap k v) where
show (GMapInt _) = undefined
后者的优点是类型检查器在解析之前不需要知道键Int
,例如show empty
(如果没有显式类型签名,它不会在第一种方法中进行编译),而是可以利用密钥类型必须始终Int
的知识。通常很有用,但在您的应用程序中可能不是正确的。