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 ABTree
s 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 Int
s with Maybe
and ABNode t
is a valid
ABTree t
, but this is clearly not the case. We need the two Int
s 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.