Design - part 1 - controlling export with modules and types

Posted on April 9, 2017

This is a series of strategies and code from fun with phantom type The paper spans a few aspects, this is a first part.

Raw

First do everything in a single file can be a good strategy for fast results. But abstracting away can also drive the design, which means it pays off for itself directly upfront.

Modules and abstraction

Separates the service provided from the implementation, in particular the data representation, chosen.

Representation choice adresses the algorithmic concerns. It influence the execution, not the service provided.

For instance this logger shields the internal representation from its user. It only exports the type constructor Logger - no data constructor - and the accompanying functions.

#!/usr/bin/env stack
-- stack script --resolver lts-8.8


{-# LANGUAGE GADTs #-}
-- nous faisons une librarie et gardons opaques les types
-- son implementation est asynchone mais son interface est synchrone
-- on utilise dans un programme/thread normal et l'utilisateur n'a pas
-- a se soucier de la maniere dont c'est implemente
-- dans son utilisation, il sera impacte par notre implementation
-- Par exemple il pourra devoir attendre s'il veut poster un message et que nous ne
-- l'avons pas encore traite.


module Logger (Logger,
               initLogger,
               logMessage,
               logStop)
    where

import           Control.Concurrent

-- en *interne*, un logger est une variable partagee
-- dans laquelle on peut ecrire / lire des commandes
data Logger where
  Logger :: MVar LoggerCommand -> Logger

data LoggerCommand where
   Message :: String -> LoggerCommand
   Stop :: MVar () -> LoggerCommand

initLogger :: IO Logger
initLogger = do
   v <- newEmptyMVar  -- et implemente selon semantique interne
   let l = Logger v
   forkIO $ spin l   -- qui est un external thread
   return l


-- fonction interne
spin l@(Logger sv) = do putStrLn "Started logger"
                        loop
  where loop = do
          cmd <- takeMVar sv  -- on ecoute en bloquant
          case cmd of
            Message msg -> do
             putStrLn msg
             loop
            Stop stopv -> do
             putStrLn "Stopped logger"
             putMVar stopv ()
             return ()

logMessage :: Logger -> String -> IO ()
logMessage (Logger sv) msg = putMVar sv (Message msg)

logStop :: Logger -> IO ()
logStop (Logger sv) = do
     stopv <- newEmptyMVar
     putMVar sv (Stop stopv)
     _ <- takeMVar stopv
     return ()


test :: IO ()
test = do
  l <- initLogger
  logMessage l "hello"
  logMessage l "Goodbye"
  logStop l


main :: IO ()
main = test

Modules pattern in haskell

One can add a file whose sole purpose is to perform abstraction : This file control what gets exported out, but has no code. Internal directory contains all the code and contian no export control, everything is public as far as it is concerned.

In bookeeper, the file bookkeeper.hs does exactly that.

More advanced strategies will be available with backpack soon

Phantom type

illustrating ideas from fun with phantom type the commented code is from here

The name “phantom types” is motivated by the fact that no actual data is ever attached to those types. Types are usually seen as a property of some data.

But when looking at what we can do with types this is a wrong idea : Types stand for static knowledge of any kind.

Example from the haskell wiki

-- you have no *data* constructor, you can not create value of that type
-- but you can still *talk* about that type, whose values might be handed to you
module MyAPI(FormData, --type constructor
             formData, -- smart constructor WE decide at what index values are created
             validate, -- normal API
             useData,
             lifSmallChange,
            ) where

import           Protolude

-- FormData is a type constructor of kind :: * -> *
-- Given any type a of kind *, FormData a is of kind *
data FormData a  where
  -- invoking the data constructor FormData with some text
  -- creates a value of type FormData a where the CALLER chooses what a
   FormData :: Text -> FormData a

-- withouth data constructor, impossible for clients to
--  - inspect what's inside a value v :: FormaData Text
--  - build a value v :: FormData i where THEY choose i
changeType :: FormData a -> FormData b
changeType (FormData str) = FormData str


data Validated
data Unvalidated

-- but WE can CHOOSE whichever type
formData :: Text -> FormData Unvalidated -- WE decide that if you call this, the data is Unvalidated
formData str = FormData str

-- we decide you can only ask use to consume validated data
useData :: FormData Validated -> IO ()
useData (FormData str) = undefined -- logic here

-- that means we FORCE YOU to call this function at *some* point before
validate :: FormData Unvalidated -> Maybe (FormData Validated)
validate (FormData str) = undefined -- logic here


-- benign operation
liftSmallChange :: (Text -> Text) -> FormData a -> FormData a
liftSmallChange = undefined -- logic which only touch small stuff


-- big operation - you have to revalidate !
liftBigChange :: (Text -> Text) -> FormData a -> FormData Unvalidated
liftBigChange = undefined -- logic which can touch anything

with this, we separate validation from action, and prevent you from tampering after it’s validated, while still allowing you maximum freedom in how you operate. we keep control, you keep freedom

Middleware pattern

one direct example of this is the middleware pattern

module Middleware(EndForYou, --type constructor
                  serve
            ) where

import           Protolude

data EndForYou where
   TheEndCtor :: EndForYou


app0 = undefined :: response
app1 = undefined :: (response -> m EndForYou) -> m EndForYou
app2 = undefined :: input -> (response -> m EndForYou) -> m EndForYou


type Application input response m = input -> (response -> m EndForYou) -> m EndForYou

serve :: Application input output m -> input -> m EndForYou
serve yourServer input = (yourServer input) process
  where process response = undefined -- actually sends the response


-- cote client il y aura
receive :: response -> next
receive = undefined

Types at Work

Types are nice, but in haskell they are not just decoration and checking. They are PART of the code, they DECIDE what to do. “Type is data”

class Sanitise a where
  sanitise :: FormData a -> FormData Validated

-- do nothing to data that is already validated
instance Sanitise Validated where
  sanitise = identity

-- sanitise untrusted data
instance Sanitise Unvalidated where
  sanitise (FormData str) = FormData (filter isAlpha str) where filter = undefined
                                                                isAlpha = undefined


a = FormData "Hello"

-- the type chooses what will be done !
_ = sanitise (a :: FormData Unvalidated)
_ = sanitise (a :: FormData Validated)

-- if it can't, it's an error, just like a syntax error
_ = sanitise a -- error !