After some headbanging, I think this is possible - or at least close enough to it to work - although it isn't very pretty to look at. (I may be on completely the wrong track here, someone knowledgeable please comment.)
It's possible (I think) to use SML's generative datatypes and functors to create abstract phantom types that cannot be referred to outside a given lexical block:
datatype ('s, 'a) Res = ResC of 's
signature BLOCK = sig
type t
val f:('s, t) Res -> t
end
signature REGION = sig
type t
val run:unit -> t
end
functor Region(B:BLOCK) :> REGION where type t = B.t = struct
type t = B.t
datatype phan = Phan
fun run () = let
val ret = (print "opening region\n"; B.f(ResC Phan))
in print "closing region\n" ; ret end
end
structure T = Region(struct
type t = int
fun f resource = ( (* this function forms the body of the "region" *)
6
)
end)
;print (Int.toString(T.run()))
This prevents the program from simply returning resource
or declaring external mutable variables it could be assigned to, which deals with most of the issue. But it can still be closed over by functions created within the "region" block, and retained that way past its supposed close point; such functions could be exported and the dangling resource reference used again, causing problems.
We can imitate ST
though, and prevent closures from being able to do anything useful with resource
by forcing the region to use a monad keyed with the phantom type:
signature RMONAD = sig
type ('s, 'a, 'r) m
val ret: ('s * 'r) -> 'a -> ('s, 'a, 'r) m
val bnd: ('s, 'a, 'r) m * ('a * 'r -> ('s, 'b, 'r) m) -> ('s, 'b, 'r) m
val get: 's -> ('s, 'a, 'r) m -> 'a * 'r
end
structure RMonad :> RMONAD = struct
type ('s, 'a, 'r) m = 's -> 's * 'a * 'r
fun ret (k, r) x = fn _ => (k, x, r)
fun bnd (m, f) = fn k => let
val (_, v, r) = m k
in f (v, r) k end
fun get k m = let val (_, v, r) = m k in (v, r) end
end
signature MBLOCK = sig
type t
val f:(t -> ('s, t, 'r) RMonad.m) (* return *)
* ('r -> ('s, string, 'r) RMonad.m) (* operations on r *)
-> ('s, t, 'r) RMonad.m
end
signature MREGION = sig
type t
val run:unit -> t
end
functor MRegion(B:MBLOCK) :> MREGION where type t = B.t = struct
type t = B.t
datatype phan = Phan
fun run () = let
val (ret, res) = RMonad.get Phan (B.f(RMonad.ret(Phan, "RESOURCE"),
(fn r => RMonad.ret(Phan, "RESOURCE") r)))
in
print("closing " ^ res ^ "\n") ; ret
end
end
structure T = MRegion(struct
type t = int
fun f (return, toString) = let
val >>= = RMonad.bnd
infix >>=
in
return 6 >>= (fn(x, r) =>
toString r >>= (fn(s, r) => (
print ("received " ^ s ^ "\n");
return (x + 1)
)))
end
end)
;T.run()
(this is a mess, but it shows my basic idea)
The resource takes the role of STRef
; if all of the provided operations on it return a monadic value instead of working directly, it will build up a chain of delayed operations that can only be executed by being returned to run
. This counters the ability of closures to retain a copy of r
outside the block because they will never actually be able to execute the op chain, being unable to return to run
, and are therefore unable to access it in any way.
Invoking T.run
twice will re-use the same "key" type, meaning this isn't equivalent to a nested forall
, but that shouldn't make a difference if there's no way to share r
between two separate invocations; which there isn't - if it can't be returned, can't be assigned outside, and any closures can't run the code that works on it. At least, I think so.