Hugh JF Chen's Blog

'My personal blog for computation!


01 Dec 2021

Separate data interface and definition when designing type

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"
    
Tags: haskell type design interface definition