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"