Skip to the content.

Open the door with types, part 3

Now, I’m going to use one type for a door but make it indexed by a phantom type to distinguish its state.

{-# LANGUAGE DataKinds,
             GADTs,
             StandaloneDeriving,
             TypeFamilies
#-}

module Door
    ( Door
    , State(Opened, Closed, Locked)
    , makeLocked
    , name
    , open
    , close
    , lock
    , unlock
    , knock
    , SingState(SOpened, SClosed, SLocked)
    , SomeDoor(SomeDoor)
) where

import Data.Kind (Constraint, Type)
import Data.Text (Text)

data State = Opened | Closed | Locked

data Door :: State -> Type where
    OpenedDoor :: Text -> Door 'Opened
    ClosedDoor :: Text -> Door 'Closed
    LockedDoor :: Text -> Text -> Door 'Locked
deriving instance Show (Door state)

As you can see, Door is now a type indexed by State. So it can be one of Door 'Opened, Door 'Closed or Door 'Locked.

Functions working on a door are pretty much similar to ones in the previous post.

name :: Door state -> Text
name (OpenedDoor name) = name
name (ClosedDoor name) = name
name (LockedDoor name _) = name

makeLocked :: Text -> Text -> Door 'Locked
makeLocked name key = LockedDoor name key

open :: Door 'Closed -> Door 'Opened
open (ClosedDoor name) = OpenedDoor name

close :: Door 'Opened -> Door 'Closed
close (OpenedDoor name) = ClosedDoor name

lock :: Text -> Door 'Closed -> Door 'Locked
lock key (ClosedDoor name) = LockedDoor name key

unlock :: Text -> Door 'Locked -> Either (Door 'Locked) (Door 'Closed)
unlock key lockedDoor@(LockedDoor name lockedKey)
    | key == lockedKey = Right $ ClosedDoor name
    | otherwise = Left lockedDoor

type family Knockable (state :: State) :: Constraint where
    Knockable 'Closed = ()
    Knockable 'Locked = ()

knock :: Knockable state => Door state -> Door state
knock = id

To implement knock, I used a type family instead of using a type class, but they’re almost identical here. With a type class, you can give each instance a different implemention, while you can only use one implementation with a type family. If what you want is to limit the types you can pass to a function, using a type family would fit better.

Now, I’ll define a singleton of State so that we can express these types in the value world.

data SingState :: State -> Type where
    SOpened :: SingState 'Opened
    SClosed :: SingState 'Closed
    SLocked :: SingState 'Locked

Then, I’ll define SomeDoor existential type which can have one of Door 'Opened, Door 'Closed or Door 'Locked.

data SomeDoor = forall state. SomeDoor (SingState state) (Door state)

Now, let’s define forceOpen.

{-# LANGUAGE DataKinds,
             GADTs,
             OverloadedStrings
#-}

import Data.Either (fromRight)
import Data.Text (Text)
import Door

forceOpen :: Text -> SingState state -> Door state -> Maybe (Door 'Opened)
forceOpen key singState door =
    case singState of
        SOpened -> Just door
        SClosed -> Just $ open $ knock door
        SLocked -> case unlock key $ knock door of
                     Right closedDoor -> Just $ open closedDoor
                     Left _ -> Nothing

forceOpenSomeDoor :: Text -> SomeDoor -> Maybe (Door 'Opened)
forceOpenSomeDoor key (SomeDoor singState door) = forceOpen key singState door

As you can see, you no longer need to define forceOpen as a method of a type class, but directly switch on its type.

This works because when singState is SOpened, for example, state must be 'Opened, so door must be Door 'Opened.

It’s still a bit annoying that you need to pass both SingState state and Door state to this function. I’ll try fixing it in the next post.