Fixpoints - MLton Standard ML Compiler (SML Compiler)

MLton MLTONWIKIVERSION | Fixpoints |
Home Index

This page discusses a framework that makes it possible to compute fixpoints over arbitrary products of abstract types. The code is from an Extended Basis library (README).

First the signature of the framework (tie.sig):

    (**
     * A framework for computing fixpoints.
     *
     * In a strict language you sometimes want to provide a fixpoint
     * combinator for an abstract type {t} to make it possible to write
     * recursive definitions.  Unfortunately, a single combinator {fix} of the
     * type {(t -> t) -> t} does not support mutual recursion.  To support
     * mutual recursion, you would need to provide a family of fixpoint
     * combinators having types of the form {(u -> u) -> u} where {u} is a
     * type of the form {t * ... * t}.  Unfortunately, even such a family of
     * fixpoint combinators does not support mutual recursion over different
     * abstract types.
     *)
    signature TIE = sig
       include ETAEXP'
       type 'a t = 'a etaexp
       (** The type of fixpoint witnesses. *)

       val fix : 'a t -> 'a Fix.t
       (**
        * Produces a fixpoint combinator from the given witness.  For example,
        * one can make a mutually recursive definition of functions:
        *
        *> val isEven & isOdd =
        *>     let open Tie in fix (function *` function) end
        *>        (fn isEven & isOdd =>
        *>            (fn 0 => true
        *>              | 1 => false
        *>              | n => isOdd (n-1)) &
        *>            (fn 0 => false
        *>              | 1 => true
        *>              | n => isEven (n-1)))
        *)

       (** == Making New Witnesses == *)

       val pure : ('a * 'a UnOp.t) Thunk.t -> 'a t
       (**
        * {pure} is a more general version of {tier}.  It is mostly useful for
        * computing fixpoints in a non-imperative manner.
        *)

       val tier : ('a * 'a Effect.t) Thunk.t -> 'a t
       (**
        * {tier} is used to define fixpoint witnesses for new abstract types
        * by providing a thunk whose instantiation allocates a mutable proxy
        * and a procedure for updating it with the result.
        *)

       val id : 'a -> 'a t
       (** {id x} is equivalent to {pure (const (x, id))}. *)

       (** == Combining Existing Witnesses == *)

       val iso : 'b t -> ('a, 'b) Iso.t -> 'a t
       (**
        * Given an isomorphism between {'a} and {'b} and a witness for {'b},
        * produces a witness for {'a}.  This is useful when you have a new
        * type that is isomorphic to some old type for which you already have
        * a witness.
        *)

       val product : 'a t * ('a -> 'b t) -> ('a, 'b) Product.t t
       (**
        * Dependent product combinator.  Given a witness for {'a} and a
        * constructor from a {'a} to witness for {'b}, produces a witness for
        * the product {('a, 'b) Product.t}.  The constructor for {'b} should
        * not access the (proxy) value {'a} before it has been fixed.
        *)

       val *` : 'a t * 'b t -> ('a, 'b) Product.t t
       (** {a *` b} is equivalent to {product (a, const b)}. *)

       val tuple2 : 'a t * 'b t -> ('a * 'b) t
       (**
        * Given witnesses for {'a} and {'b} produces a witness for the product
        * {'a * 'b}.
        *)

       (** == Particular Witnesses == *)

       val function : ('a -> 'b) t
       (** Witness for functions. *)
    end

`fix` is a type-indexed function. The type-index parameter to `fix` is called a "witness". To compute fixpoints over products, one uses the `*`` operator to combine witnesses. To provide a fixpoint combinator for an abstract type, one implements a witness providing a thunk whose instantiation allocates a fresh, mutable proxy and a procedure for updating the proxy with the solution. Naturally this means that not all possible ways of computing a fixpoint of a particular type are possible under the framework. The `pure` combinator is a generalization of `tier`. The `iso` combinator is provided for reusing existing witnesses.

Note that instead of using an infix operator, we could alternatively employ an interface using Fold. Also, witnesses are eta-expanded to work around the value restriction, while maintaining abstraction.

Here is the implementation (tie.sml):

    structure Tie :> TIE = struct
       open Product
       infix &
       type 'a etaexp_dom = Unit.t
       type 'a etaexp_cod = ('a * 'a UnOp.t) Thunk.t
       type 'a etaexp = 'a etaexp_dom -> 'a etaexp_cod
       type 'a t = 'a etaexp
       fun fix aT f = let val (a, ta) = aT () () in ta (f a) end
       val pure = Thunk.mk
       fun iso bT (iso as (_, b2a)) () () = let
          val (b, fB) = bT () ()
       in
          (b2a b, Fn.map iso fB)
       end
       fun product (aT, a2bT) () () = let
          val (a, fA) = aT () ()
          val (b, fB) = a2bT a () ()
       in
          (a & b, Product.map (fA, fB))
       end
       (* The rest are not primitive operations. *)
       fun op *` (aT, bT) = product (aT, Fn.const bT)
       fun tuple2 ab = iso (op *` ab) Product.isoTuple2
       fun tier th = pure ((fn (a, ua) => (a, Fn.const a o ua)) o th)
       fun id x = pure (Fn.const (x, Fn.id))
       fun function ? =
           pure (fn () => let
                       val r = ref (Basic.raising Fix.Fix)
                    in
                       (fn x => !r x, fn f => (r := f ; f))
                    end) ?
    end

Let's then take a look at a couple of additional examples.

Here is a naive implementation of lazy promises:

    structure Promise :> sig
       type 'a t
       val lazy : 'a Thunk.t -> 'a t
       val force : 'a t -> 'a
       val Y : 'a t Tie.t
    end = struct
       datatype 'a t' =
          EXN of exn
        | THUNK of 'a Thunk.t
        | VALUE of 'a
       type 'a t = 'a t' Ref.t
       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 Y ? = Tie.tier (fn () => let
                                 val r = lazy (raising Fix.Fix)
                              in
                                 (r, r <\ op := o !)
                              end) ?
    end

An example use of our naive lazy promises is to implement equally naive lazy streams:

    structure Stream :> sig
       type 'a t
       val cons : 'a * 'a t -> 'a t
       val get : 'a t -> ('a * 'a t) Option.t
       val Y : 'a t Tie.t
    end = struct
       datatype 'a t = IN of ('a * 'a t) Option.t Promise.t
       fun cons (x, xs) = IN (Promise.lazy (fn () => SOME (x, xs)))
       fun get (IN p) = Promise.force p
       fun Y ? = Tie.iso Promise.Y (fn IN p => p, IN) ?
    end

Note that above we make use of the `iso` combinator. Here is a finite representation of an infinite stream of ones:

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

Last edited on 2007-08-25 21:26:24 by VesaKarvonen.