一个好的心态是,从编译器的角度来看,每一种可能是每个类的实例。当一个类型不是Show的实例时,这只是意味着还没有找到instance,可能还没有写,但并不是说它不存在。
方法一
...因此,试图根据一个类型是否是一个类的实例来做出决定确实存在根本性的缺陷。然而,你能够要做的是编写一个明确区分这种区别的类。对于Show,这可能只是
class MaybeShow a where
showIfPossible :: a -> Maybe a
一个通用的版本是将以下内容包裹在 Show 类中:
{-# LANGUAGE GADTs #-}
data ShowDict a where
ShowDict :: Show a => ShowDict a
class MaybeShow a where
maybeShowDict :: Maybe (ShowDict a)
然后
{-# LANGUAGE TypeApplications, ScopedTypeVariables, UnicodeSyntax #-}
showIfPossible :: ∀ a . MaybeShow a => Maybe (a -> String)
showIfPossible = fmap (ShowDict -> show) (maybeShowDict @a)
无论哪种方式,这仍然意味着您有 MaybeShow 约束污染了您的代码库——这在某种意义上优于 Show,因为它不排除不可显示的类型,但在某种意义上也更糟,因为它需要为所有类型添加实例您需要使用的类型(即使它们已经有一个 Show 实例)。
方法二
您似乎已经考虑过将约束添加到数据类型。虽然旧语法 data Show a => MyTree a = ... 确实不应该使用,但它是可以将实例封装在data 中。事实上,我已经在上面用ShowDict 做到了。与其通过 MaybeShow 约束隐式获取,您还可以选择将其添加到您的数据类型中:
data MyTree a = Node { val :: a
, showable :: Maybe (ShowDict a)
, left :: Maybe (MyTree a)
, right :: Maybe (MyTree a) }
当然,如果您使用 Show 实例只是为了显示此特定节点的 val,那么您也可以将结果放在那里:
data MyTree a = Node { val :: a
, valDescription :: Maybe (String)
, left :: Maybe (MyTree a)
, right :: Maybe (MyTree a) }
当然,现在您正在以不同的方式污染您的代码库:每个函数产生MyTree 值需要获取 Show 实例,或者决定不能。不过,这可能影响较小,尤其是如果 MyTree 只是一个示例,并且您有更多仅适用于抽象容器的功能。
方法三
至少对于调试的特定情况,以及其他一些用例,最好使用单独的方法打开和关闭 Show 要求。最暴力的方法是一个很好的旧预处理器标志:
{-# LANGUAGE CPP #-}
#define DEBUGMODE
-- (This could be controlled from your Cabal file)
prettyPrint ::
#ifdef DEBUGMODE
Show a =>
#endif
MyTree a -> String
#ifdef DEBUGMODE
prettyPrint (Show a => ...) t = show (val t)
#else
prettyPrint t = show "?"
#endif
更精致一点的是约束同义词和合适的调试功能,可以在一个地方换掉:
{-# LANGUAGE ConstraintKinds #-}
#ifdef DEBUGMODE
type DebugShow a = Show a
debugShow :: DebugShow a => a -> String
debugShow = show
#else
type DebugShow a = ()
debugShow :: DebugShow a => a -> String
debugShow _ = "?"
#else
PrettyPrint :: DebugShow a => MyTree a -> String
PrettyPrint t = debugShow (val t)
后者再次用约束污染代码库,但您永远不需要为这些编写任何新实例。
CPP 是一个非常生硬的工具,因为它需要在编译期间全局选择是否需要 Show。但它也可以通过专用的类型级标志进行更严格的限制:
{-# LANGUAGE TypeFamilies, DataKinds #-}
data DebugMode = NoDebug | DebugShowRequired
type family DebugShow mode a where
DebugShow 'NoDebug a = ()
DebugShow 'DebugShowRequired a = Show a
class KnownDebugMode (m :: DebugMode) where
debugShow :: DebugShow m a => a -> String
instance KnownDebugMode NoDebug where
debugShow _ = "?"
instance KnownDebugMode DebugShowRequired where
debugShow = show
{-# LANGUAGE AllowAmbiguousTypes #-}
prettyPrint :: ∀ m a . DebugShow m a => MyTree a -> String
prettyPrint t = debugShow (val t)
这看起来很像方法 1,但好处是您不需要为单个 a 类型创建任何新实例。