[MLton-user] Fixpoints

Stephen Weeks sweeks@sweeks.com
Thu, 3 Aug 2006 02:03:10 -0700


> I'm not sure I like the uncurrying of fix or the new specification
> of iso, but those are largely stylistic issues.

Yep.  Those changes both follow two long-standing MLton conventions
that have proved extremely useful:

  1. Only use currying if there is actual staging of computation going
     on.
  2. Always take the object of the type being described by the
     signature as the first argument to a function.

> >     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.

Good point, but the problem is not so much the definition of u, the
problem is that I exposed too much in the interface.  One can tweak my
interface so that it only exposes that "t" is an arrow type, and hence
can be eta-expanded to circumvent the value restriction.

> 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.

I considered that, and thought that I would stick with the more
general type that I proposed, because it allows the post-fixpoint
tying of the knot to do some interesting computation.  However, I
admit it's not clear if that's useful, and drop it until someone shows
that it is.  If one ever needs to go back, it should be easy enough to
add a more general tier function to the interface and re-implement Fix
without changing any old clients.

> 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.

I think eta-expansion is both preferable and leads to simpler types
and implementation (once I got the interface right).

Here is my proposal incorporating the above changes.  Note that the
"t" type is all that is used in the specifications -- I think that is
simpler that having a client need to understand both "t" and "t'".

signature FIX =
   sig
      type 'a t1
      type 'a t2
      type 'a t = 'a t1 -> 'a t2

      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 -> unit)) thunk -> 'a t
  end

The complete code is below -- it is a minor modification of the
earlier code that I sent.

--------------------------------------------------------------------------------

datatype ('a, 'b) product = & of 'a * 'b
infix &

type 'a thunk = unit -> 'a
type ('a, 'b) emb = ('a -> 'b) * ('b -> 'a)

signature FIX =
   sig
      type 'a t1
      type 'a t2
      type 'a t = 'a t1 -> 'a t2

      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 -> unit)) thunk -> 'a t
  end

structure Fix :> FIX =
   struct
      type 'a t1 = unit
      type 'a t2 = 'a * ('a -> unit)
      type 'a t = 'a t1 -> 'a t2

      exception Fix
      
      val tier = id
         
      fun fix (t, f) =
         let
            val (a, a') = t ()
            val () = a' (f a)
         in
            a
         end
      
      fun (a ^ b) () =
         let
            val (a, a') = a ()
            val (b, b') = b ()
         in
            (a & b, fn a & b => (a' a; b' b))
         end
      
      fun iso (a, (a2b, b2a)) () =
         let
            val (a, a') = a ()
         in
            (a2b a, a' o b2a)
         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)
                          in
                             (fn a => !r a, fn f' => r := f')
                          end) ?
   end

val isEven & isOdd =
   Fix.fix
   (let open Fix Fn in 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 p = lazy (fn () => raise Fix.Fix)
                        in
                           (p, fn x => p := !x)
                        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 (Promise.T, (IN, fn IN p => p)) ?
   end

val ones =
    let open Fix Stream
    in fix (T, fn ones => cons (1, ones))
    end