motor-0.3.0: examples/Door.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Main where
import Prelude
import Control.Concurrent (threadDelay)
import Control.Monad.Indexed
import Control.Monad.IO.Class
import Data.Row.Records
import GHC.OverloadedLabels
import Motor.FSM
-- * Protocol (Abstract state types)
-- We only use marker types for states in the Door protocol.
data Open
data Closed
class MonadFSM m => Door m where
-- The associated type lets the instance choose the concrete state
-- data type.
type State m :: * -> *
-- Events:
initial :: Name n -> Actions m '[ n !+ State m Closed ] r ()
open :: Name n -> Actions m '[ n := State m Closed !--> State m Open ] r ()
close :: Name n -> Actions m '[ n := State m Open !--> State m Closed ] r ()
end :: Name n -> Actions m '[ n !- State m Closed ] r ()
-- * Implemention (Concrete types)
--
-- This could be in another module, hiding the constructors.
newtype ConsoleDoor m (i :: Row *) (o :: Row *) a =
ConsoleDoor { runConsoleDoor :: FSM m i o a }
deriving (IxFunctor, IxPointed, IxApplicative, IxMonad, MonadFSM)
run :: Monad m => ConsoleDoor m Empty Empty () -> m ()
run = runFSM . runConsoleDoor
deriving instance Monad m => Functor (ConsoleDoor m i i)
deriving instance Monad m => Applicative (ConsoleDoor m i i)
deriving instance Monad m => Monad (ConsoleDoor m i i)
instance (MonadIO m) => MonadIO (ConsoleDoor m i i) where
liftIO = ConsoleDoor . liftIO
data DoorState s where
Open :: DoorState Open
Closed :: DoorState Closed
instance Show (DoorState s) where
show Open = "Open"
show Closed = "Closed"
logLn :: (MonadIO m) => String -> m ()
logLn = liftIO . putStrLn
logDoor :: (MonadIO m, HasType n (DoorState s) i) => Name n -> ConsoleDoor m i i ()
logDoor n =
get n
>>>= \s -> logLn ("Door is now " ++ show s)
-- Extremely boring implementation:
instance (MonadIO m) => Door (ConsoleDoor m) where
type State (ConsoleDoor m) = DoorState
initial n = new n Closed
open n = enter n Open >>> logDoor n
close n = enter n Closed >>> logDoor n
end = delete
-- * Runner Program
-- This uses the protocol to define a program using the Door protocol.
sleep :: (MonadIO (m i i)) => Int -> m (i :: Row *) (i :: Row *) ()
sleep seconds = liftIO (threadDelay (seconds * 1000000))
confirm :: (MonadIO (m i i)) => String -> m (i :: Row *) (i :: Row *) Bool
confirm s = liftIO (putStrLn s >> ("y" ==) <$> getLine)
type OpenAndClose m n o c =
( Door m
-- TODO: Can these constraints be added to the Sugar module
-- automatically?
, Modify n (State m Open) c ~ o
, Modify n (State m Closed) o ~ c
, (o .! n) ~ State m Open
, (c .! n) ~ State m Closed
, (o .- n) ~ (c .- n)
)
type OpenAndCloseIO m n o c =
( OpenAndClose m n o c
, MonadIO (m o o)
, MonadIO (m c c)
)
inClosed :: OpenAndCloseIO m n o c => Name n -> m c (c .- n) ()
inClosed door = confirm "Open door?" >>>= \case
True -> open door >>>= const (inOpen door)
False -> end door
inOpen :: OpenAndCloseIO m n o c => Name n -> m o (o .- n) ()
inOpen door = confirm "The door must be closed. OK?" >>>= \case
True -> close door >>>= const (inClosed door)
False -> inOpen door
-- The program initializes a door, and starts the looping between
-- open/closed.
main :: IO ()
main = run $ initial #door >>>= const (inClosed #door)
{- $example-run
Running this program can look like this:
>>> main
Open door?
y
Door is now Open
The door must be closed. OK?
y
Door is now Closed
Open door?
y
Door is now Open
The door must be closed. OK?
n
The door must be closed. OK?
n
The door must be closed. OK?
n
The door must be closed. OK?
y
Door is now Closed
Open door?
n
-}