[MLton-user] Fixpoints
Vesa Karvonen
vesa.karvonen@cs.helsinki.fi
Wed, 2 Aug 2006 15:48:50 +0300
Quoting Stephen Weeks <sweeks@sweeks.com>:
> I was reading through
>
> http://mlton.org/Fixpoints
>
> and had an idea for simplifying things.
I was going to ask for feedback on the page once I got another page (on
type-indexed functions) ready and before linking them to the StandardML
page, but now I don't have to.
> The idea is to hide the type of the slot used to construct fixpoints
> underneath a closure, so that it doesn't appear in the type of tiers.
Excellent idea! This is actually the second time (that I recall) that
I've missed a similar simplification opportunity. I'll have to make a
mental note to consider using closures whenever a signature contains extra
type variables that reveal implementation details.
> As a minor change, I think it is also cleaner to include the thunk used
> to work around the value restriction directly in the type of tiers.
> Those changes and some other minor cleanup lead to the following
> signature.
>
> signature FIX =
> sig
> type 'a u
> type 'a t = 'a u thunk
>
> exception Fix
>
> val ^ : 'a t * 'b t -> ('a, 'b) product t
> val fix: 'a t * ('a -> 'a) -> 'a
> val iso: 'a t * ('a, 'b) emb -> 'b t
> val tier: ('a * ('a -> 'a)) thunk -> 'a t
> end
The simplified tier types look good. I'm not sure I like the uncurrying
of fix or the new specification of iso, but those are largely stylistic
issues. One thing I've been wondering about is whether other tier
building combinators (in addition to iso and ^) would make sense. A
couple of less cosmetic issues below.
> datatype 'a u = U of ('a * ('a -> 'a))
> type 'a t = 'a u thunk
The above definition of u has a problem. Namely, a user can now
instantiate the tier thunk. Consider the following kind of code:
let
val T = MyType.T () (* Instantiates the tier thunk *)
val T = fn () => T
in
(fix T (* ... *), fix T (* ... *))
end
The same slot/knot would tied twice, which (I think) shouldn't be allowed.
To fix this problem, the u type also needs to be thunked, so that only a
call to the fix function instantiates a tier.
Look at the tier closure specifications:
[...]
> (f, fn f' => (r := f'; f))
^ ^
[...]
> (p, fn x => (p := !x; p))
^ ^
[...]
As you can see, both of the closures return the same value that is already
returned as the "knot". It seems that the type of the tiers should be
changed to
> val tier: ('a * 'a effect) thunk -> 'a t
where
type 'a effect = 'a -> unit
In other words, the tying of the knot using this fixpoint framework is
inherently imperative and it leads to slightly simpler code when this is
reflected in the types.
Also, I'm not sure whether to prefer ordinary eta-expansion
> fun T ? = Fix.iso (Promise.T, (IN, fn IN p => p)) ?
or just plain thunking
> fun T () = Fix.iso (Promise.T, (IN, fn IN p => p))
leading to slightly different types.
Below is a modified implementation with the simplified tier type, thunked
abstract tiers (to eliminate the retying problem), curried fix, and plain
thunking of tier implementations.
-Vesa Karvonen
fun cross (f, g) (x, y) = (f x, g y)
fun id x = x
fun dup x = (x, x)
datatype ('a, 'b) product = & of 'a * 'b
infix &
type 'a uop = 'a -> 'a
type 'a fix = 'a uop -> 'a
type 'a thunk = unit -> 'a
type 'a effect = 'a -> unit
type ('a, 'b) ep = ('a -> 'b) * ('b -> 'a) (* embedding/projection pair *)
signature FIX =
sig
type 'a t'
type 'a t = 'a t' thunk
exception Fix
val fix : 'a t -> 'a fix
val ^ : 'a t * 'b t -> ('a, 'b) product t
val tier : ('a * 'a effect) thunk -> 'a t'
val iso : ('a, 'b) ep -> 'b t -> 'a t'
end
structure Fix :> FIX =
struct
type 'a t' = ('a * 'a effect) thunk
type 'a t = 'a t' thunk
exception Fix
val tier = id
fun fix a f =
let val (a, ta) = a () ()
in ta (f a) : unit ; a
end
fun (a ^ b) () () =
let val (a, ta) = a () ()
val (b, tb) = b () ()
in (a & b, fn a & b => (ta a : unit ; tb b : unit))
end
fun iso (a2b, b2a) b () =
let val (b, tb) = b () ()
in (b2a b, tb o a2b)
end
end
structure Fn :>
sig
val T : ('a -> 'b) Fix.t
end = struct
fun T () =
Fix.tier (fn () =>
let val r = ref (fn _ => raise Fix.Fix)
fun f x = !r x
in (f, fn f' => r := f')
end)
end
val isEven & isOdd =
let open Fix Fn in fix (T^T) end
(fn isEven & isOdd =>
(fn 0w0 => true
| 0w1 => false
| n => isOdd (n-0w1)) &
(fn 0w0 => false
| 0w1 => true
| n => isEven (n-0w1)))
structure Promise :>
sig
type 'a t
val lazy : 'a thunk -> 'a t
val force : 'a t -> 'a
val T : 'a t Fix.t
end = struct
datatype 'a t' = EXN of exn
| THUNK of 'a thunk
| VALUE of 'a
withtype 'a t = 'a t' ref
fun lazy f = ref (THUNK f)
fun force t =
case !t of
EXN e => raise e
| THUNK f =>
(t := VALUE (f ())
handle e => t := EXN e
; force t)
| VALUE v => v
fun T () =
Fix.tier
(fn () =>
let val r = lazy (fn () => raise Fix.Fix)
in (r, fn r' => r := !r')
end)
end
structure Stream :>
sig
type 'a t
val cons : 'a * 'a t -> 'a t
val get : 'a t -> ('a * 'a t) option
val T : 'a t Fix.t
end = struct
datatype 'a t = IN of ('a * 'a t) option Promise.t
fun cons (x, xs) = IN (Promise.lazy (fn () => SOME (x, xs)))
fun get (IN p) = Promise.force p
fun T () = Fix.iso (fn IN p => p, fn p => IN p) Promise.T
end
val ones =
let open Fix Stream
in fix T (fn ones => cons (1, ones))
end