Skip to content

Exporting Opaque or Transparent Types

The export of the data type ABTree deserves a little closer scrutiny. As listed in the module header right now, ABTree is exported as an opaque type. Its data constructors are not exported. This means that any module that imports the ABTree type from Data.ABTree cannot pattern match against the data costructors, and thus cannot create ABTrees directly, nor can it inspect the contents of an ABTree. To the "outside world", an ABTree is a black box that can be created only using abEmptyTree and can be manipulated only using abElem, abInsert, and abDelete. Again, this is very similar to the behaviour of a class in Java, which exports a number of public methods that can be used to interact with objects of this class, but the data members of these objects and helper methods necessary to implement the public methods are private and thus hidden from the outside world.

Opaque data types are the correct choice when the contents of objects of such a type need to satisfy certain consistency conditions that aren't enforced by the type definition itself. For example, the type

data ABTree t = ABTree Int Int (Maybe (ABNode t))

says that any combination of two Ints with Maybe and ABNode t is a valid ABTree t, but this is clearly not the case. We need the two Ints to be positive, we need that \(b \ge 2a\), and we need that all the nodes in the tree have degree between \(a\) and \(b\) or, in the case of the root, between \(2\) and \(b\). abInsert and abDelete maintain this invariant, but given access to ABTree's data constructor, a user may use it to manually create a tree that does not satisfy these conditions. By not exposing the data constructor, we prevent this and ensure that all ABTree's created in our code are valid \((a,b)\)-trees.

For other data types, any value of such a type is valid. There are no consistency conditions not explicitly encoded in the type definition itself. Maybe a is one such type. Using the type constructors, we can construct values Nothing and Just x, where x is some value of type a. Any such value is a valid value of type Maybe a. In this case, it is useful to export the data constructors because having access to the data constructors allows the user to pattern match against them, making the type more convenient to work with.

To export the data constructors of a data type, we add (..) after the type name in the export list, like so:

module Data.ABTree
    ( ABTree(..)
    , ABNode
    , abEmptyTree
    , abElem
    , abInsert
    , abDelete
    ) where

In this case, the data constructor ABTree of the ABTree type is exported. Since this data constructor takes a value of type Maybe (ABNode t) as argument, we also need to export the ABNode type now. However, we chose to export the ABNode type opaquely, so the ability of the user to mess with the contents of an \((a,b)\)-tree is still limited.

Since the internals of the ABTree type are now exposed, we call such a type a transparent type.

Note that it is also possible to export only a subset of the data constructors of a given type. To this end, we list the data constructors explicitly after the type name, as in

module Data.ABTree
    ( ABTree(ABTree)
    , ABNode
    , abEmptyTree
    , abElem
    , abInsert
    , abDelete
    ) where

Personally, I find this practice questionable. If we do not expose all data constructors, then writing exhaustive pattern matches is at least difficult if not impossible. Thus, the primary reason to expose only some data constructors isn't to support pattern matching but to support the manual creation of certain values of this type. If that's the goal, it seems better to me to export the type opaquely, along with helper functions that can be used to construct values of this type.