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.