Prevented
The first thing a consumer will see is that the type doesn't appear in Haddock documentation. In the documentation for f
and g
, the type T
will not be hyperlinked like an exported type. This may prevent a casual reader from discovering T
's class instances.
More importantly, a consumer cannot doing anything with T
at the type level. Anything that requires writing a type will be impossible. For instance, a consumer cannot write new class instances involving T
, or include T
in a type family. (I don't think there's a way around this...)
At the value level, however, the main limitation is that a consumer cannot write a type annotation including T
:
> :t (f . read) :: Read b => String -> IO (A.T b)
<interactive>:1:39: Not in scope: type constructor or class `A.T'
Not prevented
The restriction on type signatures is not as significant a limitation as it appears. The compiler can still infer such a type:
> :t f . read
f . read :: Read b => String -> IO (A.T b)
Any value expression within the inferrable subset of Haskell may therefore be expressed regardless of the availability of the type constructor T
. If, like me, you're addicted to ScopedTypeVariables
and extensive annotations, you may be a little surprised by the definition of unT'
below.
Furthermore, because typeclass instances have global scope, a consumer can use any available class functions without additional limitation. Depending on the classes involved, this may allow significant manipulation of values of the unexposed type. With classes like Functor
, a consumer can also freely manipulate type parameters, because there's an available function of type T a -> T b
.
In the example of T
, deriving Show
of course exposes the "internal" Int
, and gives a consumer enough information to hackishly implement unT
:
-- :: (Show a, Read a) => T a -> (Int, a)
unT' = (read . strip . show') `asTypeOf` (mkPair . g)
where
strip = reverse . drop 1 . reverse . drop 9
-- :: T a -> String
show' = show `asTypeOf` (mkString . g)
mkPair :: t -> (Int, t)
mkPair = undefined
mkString :: t -> String
mkString = undefined
> :t unT'
unT' :: (Show b, Read b) => A.T b -> (Int, b)
> x <- f "x"
> unT' x
(-29353, "x")
Implementing mkT'
with the Read
instance is left as an exercise.
Deriving something like Generic
will completely explode any idea of containment, but you'd probably expect that.
Prevented?
In the corners of Haskell where type signatures are necessary or where asTypeOf
-style tricks don't work, I guess not exporting the type constructor could actually prevent a consumer from doing something they could with the export list (f, g, T())
.
Recommendation
Export all type constructors that are used in the type of any value you export. Here, go ahead and include T()
in your export list. Leaving it out doesn't accomplish anything other than muddying the documentation. If you want to expose an purely abstract immutable type, use a newtype
with a hidden constructor and no class instances.