Doing More with Modules and Types

Writing code that can't go wrong

Doug Beardsley (Soostone Inc)

Programmers will ALWAYS make mistakes

Two Misconceptions

Outline

Example: Stack

module Stack where

newtype Stack a = Stack { unStack :: [a] }

push :: a -> Stack a -> Stack a
push a (Stack as) = Stack (a:as)

pop :: Stack a -> (Maybe a, Stack a)
pop (Stack []) = (Nothing, Stack [])
pop (Stack (a:as)) = (Just a, Stack as)

emptyStack :: Stack a
emptyStack = Stack []

Encapsulation

Module Syntax

module Stack where

Module Syntax

module Stack where
module Stack
  ( Stack(..)
  ) where

Module Syntax

module Stack where
module Stack
  ( Stack(..)
  ) where
module Stack
  ( Stack
  ) where

Encapsulation with Modules

module Stack
  ( Stack
  , push, pop, emptyStack
  ) where

newtype Stack a = Stack { unStack :: [a] }

push :: a -> Stack a -> Stack a
push a (Stack as) = Stack (a:as)

pop :: Stack a -> (Maybe a, Stack a)
pop (Stack []) = (Nothing, Stack [])
pop (Stack (a:as)) = (Just a, Stack as)

emptyStack :: Stack a
emptyStack = Stack []

Real World: Encapsulation in Snap

newtype Snap a = Snap
    { unSnap :: StateT SnapState (Iteratee ByteString IO) (Maybe (Either Response a)) }
newtype Snap a = Snap
    { unSnap :: StateT SnapState (Iteratee ByteString IO) (SnapResult a) }

What if you need more flexibility?

Example: Enforcing Invariants

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text
    , teamAddress :: Address
    }

data Address = Address
    { addrStreet  :: Text
    , addrCity    :: Text
    , addrState   :: Text
    , addrPostal  :: Text
    , addrCountry :: Text
    }

Example: Enforcing Invariants

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text -- Duplicate data
    , teamAddress :: Address
    }

data Address = Address
    { addrStreet  :: Text
    , addrCity    :: Text
    , addrState   :: Text
    , addrPostal  :: Text
    , addrCountry :: Text -- Duplicate data
    }

Solution: Smart Constructor Pattern

module Types.Team
  ( Team -- Note this is not Team(..)
  , mkTeam
  , teamName
  ) where

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text -- Duplicate data
    , teamAddress :: Address
    }

mkTeam :: Text -> Address -> Team
mkTeam name addr = Team name (addrCountry addr) addr

Solution: Smart Constructor Pattern

module Types.Team
  ( Team -- Note this is not Team(..)
  , mkTeam
  , teamName
  ) where

data Team = Team
    { teamName    :: Text
    , _teamCountry :: Text -- Duplicate data
    , _teamAddress :: Address
    }

mkTeam :: Text -> Address -> Team
mkTeam name addr = Team name (addrCountry addr) addr

Analysis

Analysis

  1. Strong static type system

Analysis

  1. Strong static type system
  2. Purity

Analysis

  1. Strong static type system
  2. Purity
  3. Module system

Analysis

  1. Strong static type system
  2. Purity
  3. Module system

Analysis

  1. Strong static type system
  2. Purity
  3. Module system

More powerful possibilities

More powerful possibilities

Example: Encapsulation and Template Haskell

module Types.Team where

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text
    , teamAddress :: Address
    }

mkPersist defCodegen [groundhog|
- entity: Team
  dbName: team
|]

Example: Encapsulation and Template Haskell

module Types.Team where

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text
    , teamAddress :: Address
    }

mkPersist defCodegen [groundhog|
- entity: Team
  dbName: team
|]

Solution: Separate Modules

module Types.Team.Smart
  ( Team
  , mkTeam
  , teamName
  ) where

data Team = Team
    { teamName    :: Text
    , teamCountry :: Text
    , teamAddress :: Address
    }

mkTeam :: Text -> Address -> Team
mkTeam name addr = Team name (addrCountry addr) addr
module Types.Team
  ( module Types.Team
  , module Types.Team.Smart
  ) where

import Types.Team.Smart

mkPersist defCodegen [groundhog|
- entity: Team
  dbName: team
|]

Benefits

Example: Snap and Ordering Constraints

Adding a newtype

newtype SnapletInit b v = SnapletInit (Initializer b v (Snaplet v))
makeSnaplet :: Text
            -> Text
            -> Maybe (IO FilePath)
            -> Initializer b v v
            -> SnapletInit b v
nestSnaplet :: ByteString
            -> SnapletLens v v1
            -> SnapletInit b v1
            -> Initializer b v (Snaplet v1)

Example: Security

Layered architecture

SecureT

module SecureT (SecureT, runSecureT, liftChecked) where
-- ...

data User = User { userCapabilities :: Set Capability }

newtype SecureT m a = SecureT (ReaderT User m a)
  deriving (Functor, Monad)

runSecureT :: SecureT m a -> User -> m a
runSecureT (SecureT action) user = runReaderT action user

getCurrentUser :: Monad m => SecureT m User
getCurrentUser = SecureT ask

liftChecked :: Monad m => Capability -> m a -> SecureT m a
liftChecked requiredCapability action = do
  user <- getCurrentUser
  if requiredCapability `Set.member` userCapabilities user
    then SecureT $ lift action
    else fail $ "Permission denied: " ++ show requiredCapability

Making it Safe

Making it abstract

class MonadChecked capability t where
  liftChecked :: Monad m => capability -> m a -> t m a

instance MonadChecked Capability SecureT where
  liftChecked requiredCapability action = do
    user <- getCurrentUser
    if requiredCapability `Set.member` userCapabilities user
      then SecureT $ lift action
      else fail $ "Permission denied: " ++ show requiredCapability

Notes:

Summary