Separate data interface and definition when designing type

Table of Contents

1 What is type design

When you explore a domain, the first thing you should do is to create a domain vocabularies which can make it easier to communicate to others. Those vocabularies are domain model and usually represented with types. Types defines the characteristic of the domain and with a well-designed type model, you can go far far away down to the problem path.

2 What is data interface

An interface means something you expose to the outside and can be used by others regardless of the implementation detains.

3 What is data definition

Data definition is the real implementation which you should hide from others.

4 how to separate the data interface from the definition

Take haskell for example, we can separate the data interface from its real definition with following design technique:

4.1 Data interface

Following create an interface for data

  • Export a smart constructor instead of the data constructor(s). By this way, we hide the data real definitions to the outside world
  • Not export the data constructor(s) will lose the feature for pattern matching. To regain this, we can define some pattern synonyms and export them with the type
  • So smart constructor and pattern synonyms create an interface for the type and the type definition is completely hidden from outside

An example would be like following:

{-# LANGUAGE GeneralizedNewtypeDeriving, PatternSynonyms #-}
module SomeModule
( TypeConstructor(PatternSynonyms, unSomeType)
, SmartConstructor
, OtherInterface
) where

newtype SomeType = SomeTypeInternalDataConstructor { unSomeType :: Text } deriving (Eq, Org, Show)

pattern SomeType :: Text -> SomeType
pattern SomeType t <- SomeTypeInternalDataConstructor { unSomeType = t }
...

4.2 Data definition

By separating the interface, we have great flexibility to define our real data in the way we want.

  • We can parse the data within the smart constructor and make sure the data is correct(kind of dependent type or refinement type)
  • We can even use view pattern to check the correctness with any function
  • We can change the data definition freely, like change the using data structure

An example as following:

{-# LANGUAGE GeneralizedNewtypeDeriving, ViewPatterns #-}
newtype SomeType = SomeTypeInternalDataConstructor { unSomeType :: Text } deriving (Eq, Org, Show)

viewPatternFunc :: SomeType -> Bool
viewPatternFunc st = (checkF1 st)
		  && (checkF2 st)
		  && (checkF3 st)
mkSomeType :: Text -> Either Text SomeType
mkSomeType t@(viewPatternFunc -> True) = Right $ SomeTypeInternalDataConstructor t
mkSomeType t@(_) = Left "Some error message"

Date: 2025-10-23

Author: Chen Jin Fen

Emacs 27.1 (Org mode 9.3)