类嵌套数据类型中的实例实现



我想知道如何在不使用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 kk除了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的知识。通常很有用,但在您的应用程序中可能不是正确的。

最新更新