A good mindset is that from the compiler's point of view, every type is potentially an instance of every class. When a type is not an instance of Show
, it just means the instance
has not been found yet, possibly not been written yet, but not that it doesn't exist.
Approach 1
...Therefore, trying to make a decision based on whether or not a type is an instance of a class is indeed quite fundamentally flawed. However, what you can do is to write a class that explicitly makes this distinction. For Show
this could simply be
class MaybeShow a where
showIfPossible :: a -> Maybe a
A generalizable version is to wrap the following around the Show
class:
{-# LANGUAGE GADTs #-}
data ShowDict a where
ShowDict :: Show a => ShowDict a
class MaybeShow a where
maybeShowDict :: Maybe (ShowDict a)
and then
{-# LANGUAGE TypeApplications, ScopedTypeVariables, UnicodeSyntax #-}
showIfPossible :: ∀ a . MaybeShow a => Maybe (a -> String)
showIfPossible = fmap (\ShowDict -> show) (maybeShowDict @a)
Either way, this would still mean you have the MaybeShow
constraint polluting your codebase – which is in a sense better than Show
as it doesn't preclude unshowable types, but in a sense also worse because it requires adding instance for all the types you need to use (even if they already have a Show
instance).
Approach 2
You already seem to have considered adding the constraint to the data type instead. And although the old syntax data Show a => MyTree a = ...
should indeed never be used, it is possible to encapsulate instances in data
. In fact I already did it above with ShowDict
. Rather than obtaining that implicitly via a MaybeShow
constraint, you can also just add it optionally to your data type:
data MyTree a = Node { val :: a
, showable :: Maybe (ShowDict a)
, left :: Maybe (MyTree a)
, right :: Maybe (MyTree a) }
Of course, if all you're using the Show
instance for is for showing the val
of this specific node, then you could instead also just put the result right there:
data MyTree a = Node { val :: a
, valDescription :: Maybe (String)
, left :: Maybe (MyTree a)
, right :: Maybe (MyTree a) }
Now of course you're polluting your codebase in a different way: every function that generates a MyTree
value needs to procure a Show
instance, or decide it can't. This likely has less of an impact though, and especially not if MyTree
is only an example and you have many more functions that just work on abstract containers instead.
Approach 3
At least for the specific case of debugging, but also some other use cases, it might be best use a separate means of turning the Show
requirement on and off. The most brute-force way is a good old preprocessor flag:
{-# 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
A bit more refined is a constraint synonym and fitting debug function, that can be swapped out in just a single place:
{-# 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)
The latter again pollutes the codebase with constraints, but you never need to write any new instances for these.
CPP
is quite a blunt tool, in that it requires selecting globally during compilation whether or not you want to require Show
. But it can also be done more confined, with a dedicated type-level flag:
{-# 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)
This looks a lot like approach 1, but the nice thing is that you don't need any new instances for individual a
types.
The way to use prettyPrint
now is to specify the debug mode with a type application. For example you could extract debug- and production-specific versions thus:
prettyPrintDebug :: Show a => MyTree a -> String
prettyPrintDebug = prettyPrint @('DebugShowRequired)
prettyPrintProduction :: MyTree a -> String
prettyPrintProduction = prettyPrint @('NoDebug)