Are export type constructors different?

Let's say I have an internal data type, T a , which 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) fa = fmap (\i -> T (i, a)) randomIO g :: T a -> a g = snd . unT 

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

+5
source share
1 answer

prevented

The first thing a consumer sees is that this type is not displayed in the Haddock documentation. In the documentation for f and g type T will not be hyperlinks, like an exported type. This may prevent the casual reader from discovering instances of class T

More importantly, the consumer cannot do anything with T at the type level. Anything that requires a type spelling will be impossible. For example, a consumer cannot write new instances of a class involving T or include T in a type family. (I don't think there is a way around this ...)

However, at the value level, the main limitation is that the user cannot write type annotations, including T :

 > :t (f . read) :: Read b => String -> IO (AT b) <interactive>:1:39: Not in scope: type constructor or class `AT' 

Not tagged

The restriction on type signatures is not as significant a limitation as it seems. The compiler can still output this type:

 > :tf . read f . read :: Read b => String -> IO (AT b) 

Any expression of an expression inside an output Haskell subset can be expressed regardless of the availability of a constructor of type T If, like me, you are addicted to ScopedTypeVariables and extensive annotations, you might be a little surprised at the unT' definition below.

In addition, since typeclass instances have global scope, the consumer can use any available class functions without additional restrictions. Depending on the respective classes, this can lead to significant manipulation of values ​​of an unexposed type. With classes such as Functor , the consumer is also free to manipulate type parameters, since there is an available function of type T a -> T b .

In the T example, the output of Show , of course, exposes the "internal" Int and gives enough information for the hacker to inject 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) => AT b -> (Int, b) > x <- f "x" > unT' x (-29353, "x") 

The mkT' implementation with the Read instance remains as an exercise.

Getting something like Generic will completely blow up any containment idea, but you probably expect it.

prevented?

In Haskell corners where signature types are needed or where asTypeOf -type tricks do not work, I think that not exporting the type constructor can actually prevent the consumer from doing something that can be done using the export list (f, g, T()) .

Recommendations

Export all type constructors that are used in the type of any value you export. Here go and include T() in your export list. Leaving this does nothing but obscure the documentation. If you want to expose a purely abstract immutable type, use newtype with a hidden constructor and class instances.

+4
source

Source: https://habr.com/ru/post/1208005/


All Articles