{-# OPTIONS -fglasgow-exts #-}
{-# OPTIONS -fallow-undecidable-instances #-}
{-
Handle-based IO with the statically assured open/close protocol:
-- one may perform i/o only when the handle is opened
-- once the handle is closed, it stays closed
-- one cannot close the handle more than once
-- at the end of the program, all handles are closed.
There is no requirement that the opening/closing of handles must be
properly nested. That is, it is not necessary to close handles in the
inverse order they have been opened. As shown in the last example
(suggested by Ken), we open one file (e.g., the configuration file) to
read the name of the main file, close the configuration file and
continue working with the second file. There are no limitation as to
how many handles may be opened or kept open concurrently.
More elaborated version of the code in multi-handle-io0.hs.
Recursive IO computations are now possible.
Alas, although this approach can work out in the absence of
exceptions, it crumbles when any exceptions arise, and the latter is
inevitable. Let's consider the code
newRgn $
do
h1 <- shOpenFile "file1"
c1 <- shReadChar
h2 <- shOpenFile "file2"
shPutChar h2 c1
shClose h1
shPutChar h2 c1
shClose h2
According to static guarantees, the open/close protocol is obeyed:
when `newRgn' computation finished, all handles opened with the region
are closed. Suppose an exception arose at the first shPutChar. Where
to put an exception handler? At newRgn? How would the handler know
that both h1 and h2 need to be closed? And if an exception arose at
the second shPutChar, then only h2 has to be closed. How would the
handler know that, given that IO exceptions don't contain the type of
the monad where they arose? Furthermore, how would an exception
handler (at newRgn) get hold of the handles to close, given that both
h1 and h2 names are out of scope at that point?
One attempt to save the day is to wrap an exception handler around
_every_ monadic operation, and to pack into the exception the state of
the monad (which is known only statically and erased at run-time -- so
we need a reification, something like Typeable) along with applicable
handles. But that is clearly not a scalable or efficient solution. The
point of an exception is that a handler is placed only at certain
points in a computation. If we check for an exception after each and
every operation, we essentially prohibit exceptions. Alas, in Haskell
exceptions are omni-present.
Thus we reach the conclusion that in the presence of exceptions (which
may occur, in principle, at any point in execution -- even so when IO
is concerned) we cannot get by with purely static gurantees. Some
dynamic checks are necessary -- which defeats this whole approach
because the conventional IO with handles already does the dynamic
check for the validity of a handle.
But see SafeHandles.hs for a hybrid solution, which is still better
than System.IO.
Joint work with Chung-chieh Shan.
-}
module MultiHandle where
import System.IO
-- ======================================================================
-- Security kernel below
-- ----------------------------------------------------------------------
-- safe handle and the SIO monad
-- The SIO monad is a VST IO monad where the state is a (heterogeneous)
-- list, which can informally be described as
--
-- kind State = [RegionD]
-- kind RegionD = (Mark, RegionLabel, Counter, ActiveLabels)
-- kind Mark = eigenvariable unque to the region
-- kind Counter = Peano numeral for producing handle labels
-- kind ActiveLabels = [Label] -- labels of the open handles
-- kind Label = Peano numeral
-- kind RegionLabel = Label -- label for the region. The top region
-- has label Z
--
-- Since we don't have kind declarations in Haskell, we have to emulate
-- above using Peano numerals and heterogeneous lists built with the type
-- constant N and the binary type constructor C h t. This reminds one
-- of emulating data structures in Scheme/Lisp using only cons, car, cdr, nil
-- and pair?.
--
-- We permit a sequence of properly nested regions.
-- Each region describes the set of handles created in that region.
-- An open handle is valid in any child region.
-- Within a region, handles can be opened and closed at will,
-- in any order. One can close handles in the parent region.
-- When the region exits, all handles opened in that region must be closed.
-- A created handle is labeled with the label of the active
-- region and inserted in that region's active label list.
--
-- Why we need regions: to be able to write iterative (generally recursive)
-- programs that open handles.
-- The type system then guarantees that one cannot create unbounded
-- amount of open handles. When used with recursive code, the type
-- system will prevent the user to close handles in the parent region.
-- That is good: otherwise, we can't guarantee that each handle is closed
-- exactly once.
type SIO si so v = VST IO si so v
sio_in :: SIO si so v -> si; sio_in = undefined
sio_out :: SIO si so v -> so; sio_out = undefined
sio :: (StateOK si, StateOK so) => IO v -> si -> so -> SIO si so v
sio = undefined
-- A handle is labeled by the eignevariable and by the label of the
-- region it is created in, as well as the label within that region
-- The eignevariable is opaque and merely prevents the escape of the handle.
-- The region label is a numeral and lets us locate the region
newtype SHandle s rl l = SHandle Handle
shandle :: Handle -> s -> rl -> l -> SHandle s rl l
shandle h _ _ _ = SHandle h
shandle_l :: SHandle s rl l -> (rl,l); shandle_l = undefined
-- well-formedness predicate for the state, or, the description of
-- the kind state. Not exported.
-- It is used only internally, to add `static kinding' to the type-level
-- programs below
class StateOK s -- non-empty list of regionD
instance RegionDOK regionD => StateOK (C regionD N)
instance (RegionDOK regionD, StateOK r) => StateOK (C regionD r)
class RegionDOK r
-- Add: in any list of active labels, all labels are unique.
instance (Nat0 regionLabel, Nat0 counter)
=> RegionDOK (mark,regionLabel, counter, activeLabels)
-- create the top region and run a computation in it
-- The resulting state of the computation should be empty as well:
-- all nested regions must be disposed of, and all handles are closed
runSIO :: (forall s. SIO (C (s,Z,Z,N) N) (C (s,Z,any,N) N) v) -> IO v
runSIO m = runVST m -- somehow, eta-expansion is needed here
-- Create a new region and run a computation there. When the region
-- is over, its list of active handle labels must be empty, i.e., N
-- The signature is a bit unwieldy. We don't have
-- the `as' construct as in OCaml. Also, we have to write the signature in
-- full because of the higher-rank type.
-- There aren't many such signatures, however.
newRegion :: (forall r.
SIO (C (r,S reglabel,Z,N)
(C (s,reglabel,cnt,active) rest))
(C (r,S reglabel,any,N)
(C (s,reglabel,cnt,active') rest'))
v) ->
SIO (C (s,reglabel,cnt,active) rest) -- input state
(C (s,reglabel,cnt,active') rest') -- result state, some handles
-- may be closed there
v
newRegion m = VST (runVST m)
-- low-level primitive to work with SHandles. They are certainly not exported.
new_shandle h = r
where
r = sio (return (shandle h s reglabel cnt)) undefined so
so = C (s,reglabel,newcnt,newlst) rest
newcnt = inc cnt
newlst = C cnt lst
C (s,reglabel,cnt,lst) rest = sio_in r
-- Check that the handle is current.
check_shandle sh@(SHandle h) f = r
where
r = sio (f h) undefined so
so = sio_in r
(reglabel, l) = shandle_l sh
(m,_,_,activelabels) = apply (FindL (RegLabelQ reglabel)) so
-- If the label can be removed, it means it is present.
_ = apply RemL (l,activelabels)
{-
-- Remove the handle from the list of current.
remove_shandle sh@(SHandle h) f = r
where
r = sio (f h) undefined so
so = sio_in r
(reglabel, l) = shandle_l sh
(m,_,_,activelabels) = apply (FindL (RegLabelQ reglabel)) so
-- If the label can be removed, it means it is present.
_ = apply RemL (l,activelabels)
-}
newtype RegLabelQ n = RegLabelQ n
instance EQN n n2 r => Apply (RegLabelQ n) (m,n2,c,ls) r
test1 = runSIO (glift (print "yes") +>>
newRegion (gret "yes again")) >>= print
-- regular handle operations
shIsEOF sh = check_shandle sh hIsEOF
-- shClose:: Handle -> IO ()
-- hFileSize:: Handle -> IO Integer
-- hFlush :: Handle -> IO ()
-- hPutStr :: Handle -> String -> IO ()
-- hPutChar :: Handle -> Char -> IO ()
-- hGetChar :: Handle -> IO Char
-- example with closing the file in the error handler
-- When opening a file, catch the exception and re-raise it by wrapping
-- into `IO error during file open exc'. So, the upward error handler can know
-- when to close the handle and when not.
-- openFile :: FilePath -> IOMode -> IO Handle
-- ----------------------------------------------------------------------
-- Variable state monad transformer
-- The state is variable and purely phantom
-- (type-level only). That at the run-time, our state-changing `monad'
-- is the same as the underlying, base monad.
class Monadish m where
gret :: a -> m p p a
gbind :: m p q a -> (a -> m q r b) -> m p r b
-- Inject regular monads to be monadish things too
newtype MW m p q a = MW{ unMW:: m a }
instance Monad m => Monadish (MW m) where
gret = MW . return
gbind (MW m) f = MW (m >>= unMW . f)
-- some syntactic sugar
infixl 1 +>>
vm1 +>> vm2 = gbind vm1 (const vm2)
infixl 1 >==
m >== f = gbind m f
-- Introduce the variable-type phantom state. It is operationally
-- indistinguishable from the base monad m.
newtype VST m si so v = VST{runVST:: m v}
-- lift into the VST monad
glift :: Monad m => m v -> VST m si si v
glift = VST
-- Pure type-level computation (attribution)
vsputL :: Monad m => m v -> si -> so -> VST m si so v
vsputL m _ _ = VST m
instance Monad m => Monadish (VST m) where
gret = VST . return
gbind (VST m) f = VST (m >>= runVST . f)
vsget :: forall m si. Monad m => VST m si si si
vsget = VST (return (undefined::si))
vsput :: Monad m => so -> VST m si so ()
vsput x = VST (return ())
-- ----------------------------------------------------------------------
-- Peano numerals, used to label handles
-- We only require the successor and comparison operations
data Z
data S a
inc :: a -> S a
inc = undefined
class Nat0 n
instance Nat0 Z
instance Nat0 n => Nat0 (S n)
data HTrue
data HFalse
__ = __
-- n1 == n2 --> HTrue
-- otherwise --> HFalse
class EQN n1 n2 r | n1 n2 -> r
instance EQN Z Z HTrue
instance EQN Z (S n) HFalse
instance EQN (S n) Z HFalse
instance EQN n1 n2 r => EQN (S n1) (S n2) r
eqn :: EQN n1 n2 r => n1 -> n2 -> r; eqn = undefined
n0 :: Z = __
n1 = inc n0
n2 = inc n1
n3 = inc n2
test_eqn1 = eqn n0 n1
test_eqn2 = eqn n1 n1
test_eqn3 = eqn n2 n1
-- ----------------------------------------------------------------------
-- A bit of HList
data N = N
data C a b = C a b
-- A heterogeneous apply operator
class Apply f a r | f a -> r where
apply :: f -> a -> r
apply = undefined -- In case we use Apply for
-- type-level computations only
-- Normal function application
instance Apply (x -> y) x y where
apply f x = f x
-- Identity
data Id = Id
instance Apply Id x x where
apply _ x = x
-- Type-level computation
-- remove a label from a list of labels
data Closure f arg -- not exported
data RemL = RemL
instance (EQN l e bf, Apply (Closure RemL bf) (l,(C e r)) w)
=> Apply RemL (l,(C e r)) w
instance Apply (Closure RemL HTrue) (l,(C l r)) r
instance Apply RemL (l,r) w => Apply (Closure RemL HFalse) (l,(C e r)) (C e w)
test_Reml1 = apply RemL (n0,(C n0 N))
-- Type error
-- test_Reml2 = apply RemL (n3,(C n0 (C n1 (C n2 N))))
test_Reml3 = apply RemL (n0,(C n0 (C n1 (C n2 N))))
test_Reml4 = apply RemL (n1,(C n0 (C n1 (C n2 N))))
test_Reml5 = apply RemL (n2,(C n0 (C n1 (C n2 N))))
-- Find an element in a heterogenous list satisfying a given predicate
-- The element must be present
newtype FindL f = FindL f
instance (Apply pred e bf, Apply (Closure (FindL pred) bf) (C e r) w)
=> Apply (FindL pred) (C e r) w
instance Apply (Closure (FindL pred) HTrue) (C e r) e
instance Apply (FindL pred) r w
=> Apply (Closure (FindL pred) HFalse) (C e r) w
test_Findl1 = apply (FindL (EqtoN n1)) (C n0 (C n1 (C n2 N)))
newtype EqtoN n = EqtoN n
instance EQN n1 n2 r => Apply (EqtoN n1) n2 r
{-
-- problem with polymorphic recursion...
reml l (C e rest) = cond (eqn e l) (Id,rest) (f, (e,l,rest))
where
f (e,l,r) = C e (reml l r)
class Cond t th el w where
cond :: t -> th -> el -> w
cond = undefined
instance (Apply f arg r, TypeCast r w) => Cond HTrue (f,arg) el w where
instance (Apply f arg r, TypeCast r w) => Cond HFalse th (f,arg) w where
test_reml1 = reml n0 (C n0 N)
-- Type error
-- test_reml2 = reml n1 (C n0 N)
test_reml3 = reml n0 (C n0 (C n1 (C n2 N)))
-- *SafeHadles> :t test_reml3
-- test_reml3 :: C (S Z) (C (S (S Z)) N)
-- test_reml4 = reml n1 (C n0 (C n1 (C n2 N)))
-- test_reml5 = reml n2 (C n0 (C n1 (C n2 N)))
-}