Let's say I have an internal data type, T a, that is used in the signature of exported functions:

module A (f, g) where

newtype T a = MkT  { unT :: (Int, a) } 
    deriving (Functor, Show, Read) -- for internal use

f :: a -> IO (T a)
f a = fmap (\i -> T (i, a)) randomIO

g :: T a -> a
g = snd . unT

What is the effect of not exporting the type constructor T? Does it prevent consumers from meddling with values of type T a? In other words, is there a difference between the export list (f, g) and (f, g, T()) here?

share|improve this question
up vote 4 down vote accepted

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 Twill 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.

share|improve this answer
1  
Not exporting the type constructor doesn't allow the user to create "invalid" values, e.g. a non-balanced tree. – Johannes Kuhn Dec 1 '14 at 1:13
2  
@JohannesKuhn - I'm not sure I understand. Are you thinking of data constructors, like MkT in the example? – Christian Conkle Dec 1 '14 at 1:14
1  
Indeed. The fact that it's legal to export a value without also exporting the constructors necessary to express its type is a bit of a wart in the language. – dfeuer Dec 1 '14 at 1:26

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.