Skip to content

Opaque Exports of Record Types

Consider the StudentRecord type we defined earlier

Banner/StudentRecord.hs
module Banner.StudentRecord where

data StudentRecord = SR
    { bannerNumber :: BannerNumber
    , name         :: String
    , address      :: String
    , transcript   :: Transcript
    }

newtype BannerNumber = B Int

data Transcript = T

Here, we place this type into a module Banner.StudentRecord that is part of a larger collection of modules that we use to implement the Banner system.

Now, a record type is a whole collection of things. First, it comes with data constructors just as any other algebraic data type, but it can also be "updated" using record update syntax and, possibly most importantly, the field names act as data accessors. Here, we have functions

bannerNumber :: StudentRecord -> BannerNumber
name         :: StudentRecord -> String
address      :: StudentRecord -> String
transcript   :: StudentRecord -> Transcript

When exporting the StudentRecord type, we now have the usual choices:

  • The internals of the StudentRecord type, including the data accessors, are part of the public interface of the Banner.StudentRecord module. In particular, we want the user to be able to create new student records using the SR data constructor, to pattern match against it, and to update StudentRecords using record update syntax.
  • A StudentRecord needs to satisfy some internal consistency conditions that aren't satisfied by every possible value constructible using the SR data constructor. In this case, we want to export the type opaquely, hiding its data constructors. This opaque export means that the data constructors and data accessors are not exported. As a consequence, outside of the Banner.StudentRecord module itself, record update syntax cannot be used to update StudentRecords.

Again, we use StudentRecord(..) in the export list of our module if we want to export StudentRecord transparently, and only StudentRecord if we want to export StudentRecord opaquely.

When exporting StudentRecord opaquely, we have the option to expose all or some of the data accessors. Remember, the main reason to export a type opaquely is to prevent the creation of values of this type that are invalid in some sense. The data accessors cannot be used to create such values; for that, we would need the data constructors of the type. In some cases, it makes sense to consider all or some of the data accessors as part of the public interface. Here, for example, it makes perfect sense to export functions that allows us to extract the banner number, name, address or transcript from a StudentRecord. To export StudentRecord opaquely but export the data accessors, we'd use the export list

module Banner.StudentRecord
    ( StudentRecord
    , BannerNumber(..)
    , Transcript(..)
    , bannerNumber
    , name
    , address
    , transcript
    ) where

We're explicitly exporting bannerNumber, name, address, and transcript along with the opaque type StudentRecord. To the user of the Banner.StudentRecord module, bannerNumber, name, address, and transcript look like ordinary functions. They have no indication whatsoever that these are in fact data accessors created by the definition of StudentRecord as a record type.