Last active
August 22, 2022 18:18
-
-
Save gelisam/9845116 to your computer and use it in GitHub Desktop.
IndexedMonad example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- in reply to http://www.reddit.com/r/haskell/comments/21mja6/make_lllegal_state_transitions_unrepresentable/ | |
-- | |
-- We implement a tiny language with three commands: Open, Close, and Get. | |
-- The first Get after an Open returns 1, the second Get returns 2, and so on. | |
-- | |
-- Get is only valid while the state is open, and | |
-- Open must always be matched by a Close. | |
-- We enforce both restrictions via the type system. | |
-- | |
-- There are two valid states: Opened and Closed. | |
-- The only legal transitions are | |
-- Closed ---Open--> Opened | |
-- Opened ---Get---> Opened | |
-- Opened ---Close-> Closed | |
-- No other Transitions are representable. | |
{-# LANGUAGE RebindableSyntax, GADTs #-} | |
import qualified Prelude as P | |
import Prelude (Num(..), Functor(..), Int, IO, (.), seq) | |
-- States, which must be represented as types | |
-- so that they can appear as type indices. | |
data Opened | |
data Closed | |
-- The only three valid transitions. | |
data Transition i j a where | |
Open :: Transition Closed Opened () | |
Close :: Transition Opened Closed () | |
Get :: Transition Opened Opened Int | |
-- A sequence of valid transitions. | |
data Program i j a where | |
Return :: a -> Program i i a | |
Command :: Transition i j a -> Program i j a | |
-- The first half of the Program ends in state `j`, | |
-- so the rest of the Program must begin in state `j`. | |
Bind :: Program i j a -> (a -> Program j k b) -> Program i k b | |
-- The only three valid commands. | |
open = Command Open | |
close = Command Close | |
get = Command Get | |
-- At runtime, we need to remember the number of times Get has been called, | |
-- but we only need to remember this while the resource is still Opened. | |
data Runtime a where | |
StateOpened :: Int -> Runtime Opened | |
StateClosed :: Runtime Closed | |
-- Run a well-formed Program which matches each Open with a Close. | |
-- We don't actually use IO, but to Open a real resource you probably would. | |
runProgram :: Program Closed Closed a -> IO a | |
runProgram = fmap P.fst . go StateClosed | |
where | |
go :: Runtime i -> Program i j a -> IO (a, Runtime j) | |
go s (Return x) = P.return (x, s) | |
go s (Bind p f) = go s p P.>>= \(x, s') -> go s' (f x) | |
go (StateOpened _) (Command Close) = P.return ((), StateClosed) | |
go (StateOpened n) (Command Get) = P.return (n, StateOpened (n + 1)) | |
go StateClosed (Command Open) = P.return ((), StateOpened 1) | |
-- Since Haskell is lazy, we must also cover the following case, | |
-- otherwise we get a "non-exhaustive patterns" warning. | |
go x (Command y) = x `seq` y | |
`seq` P.error "expected x or y to be undefined" | |
-- Use RebindableSyntax to change the types of return and (>>=). | |
class IndexedMonad m where | |
return :: a -> m i i a | |
(>>=) :: m i j a -> (a -> m j k b) -> m i k b | |
-- More definitions required by RebindableSyntax. | |
t >> p = t >>= (\() -> p) | |
fail = P.error | |
-- Program is not a Monad because `m i j` doesn't unify with `m j k`. | |
-- It is, however, an IndexedMonad. | |
instance IndexedMonad Program where | |
return = Return | |
(>>=) = Bind | |
-- If you try to reorder the operations | |
-- so that a Get doesn't occur between an Open and a Close, or | |
-- if Open is not matched by a Close, you get a type error. | |
program :: Program Closed Closed [Int] | |
program = do | |
open | |
x <- get | |
y <- get | |
close | |
return [x, y] -- [1, 2] | |
-- | | |
-- >>> main | |
-- [1,2] | |
main :: IO () | |
main = runProgram program P.>>= P.print |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is a version which doesn't simplify
IndexedMonad
by hardcodingTransition
. The differences are:(>>=)
's first argument is anm
instead of aTransition
.Bind
's first argument is aProgram
instead of aTransition
.Program
has an extra constructor,Command
, which wraps aTransition
.runClosed
andrunOpened
have been merged into a single functiongo
.Runtime
allowsgo
to either request anInt
or no extra value depending on whether the resource is currentlyOpened
orClosed
.