Search code examples
haskelltypesdomain-modelrefinement-type

Simple Refinement Types in Haskell


From Scott Wlaschin's blog post and Book "Domain Modeling made Functional", and from Alexis King's post, I take that a domain model should encode as much information about the domain as is practical in the types, so as to "make illegal states unrepresentable" and to obtain strong guarantees that allows me to write domain logic functions that are total.

In basic enterprise applications, we have many basic domain types like street names, company names, cities and the like. To represent them in a form that prevents most errors later on, I would like to use a type that lets me

  • restrict the maximum and minimum number of characters.
  • specify the subset of characters that may be used,
  • add additional constraints, like no leading or trailing whitespace.

I can think of two ways to implement such types: As custom abstract data types with smart constructors and hidden data constructors, or via some type-level machinery (I vaguely read about refinement types? Can such types be represented via some of the newer language extensions? via LiquidHaskell?). Which is the sensible way to go? Which approach most easily works with all the functions that operate on regular Text, and how can I most easily combine two or more values of the same refined type, map over them, etc.?

Ideally, there would be a library to help me create such custom types. Is there?


Solution

  • following Alexis King's blog, I'd say that a suitable solution would be something like below. Of course, other solutions are posible.

    import Control.Monad (>=>)
    
    newtype StreetName = StreetName {getStreetName :: String}
    
    -- compose all validations and wrap them in new constructor.
    asStreetName :: String -> Maybe StreetName
    asStreetName = StreetName <$> rightNumberOfChars >=> rightCharSubset >=> noTrailingWhiteSpace
    
    -- This funcs will process the string, and produce another validated string or nothing. 
    rightNumberOfChars :: String -> Maybe String
    rightNumberOfChars s = {- ... -}
    
    rightCharSubset :: String -> Maybe String
    rightCharSubset s = {- ... -}
    
    noTrailingWhiteSpace :: String -> Maybe String
    noTrailingWhiteSpaces = {- ... -}
    
    main = do
      street <- readStreetAsString
      case asStreetName street of
        Just s  -> {- s is now validated -}
        Nothing -> {- handle the error -}
    
    

    make StreetName a hiden constructor, as use asStreetName as a smart constructor. Remember that other functions should use StreetName instead of String in the type, to make sure that data is validated.