[MLton] handling a variable number of arguments in SML

Stephen Weeks MLton@mlton.org
Wed, 24 Aug 2005 04:50:06 -0700


>       val () = p (fold (0, op +) (a 1) (a 2) (a 3) $)
> 
>       val () = p (fold (0, op +) (a 1 & a 2 & a 3))

The continuation approach can be syntactically simplified even
further, so that one need not even parenthesize the arguments.  E.g.,
one can write

        val () = p (fold (0, op +) a 1 a 2 a 3 $)

Here's the implementation for the basic case.

----------------------------------------------------------------------
signature S =
   sig
      type 'k v
      type 'k u = 'k v -> 'k

      val $ : int v
      val f: 'k u
      val a: (int -> 'k u) v
   end

functor F (S: S) =
   struct
      open S

      fun p i = print (concat [Int.toString i, "\n"])
      val () = p (f a 1 a 2 a 3 $)
   end
  
structure S: S =
   struct
      type 'k v = int -> 'k
      type 'k u = 'k v -> 'k

      fun $ i = i
      fun f kv = kv 0
      fun a i j kv = kv (i + j)
   end

structure Z = F (S)
----------------------------------------------------------------------

And here's the generalization to fold.

----------------------------------------------------------------------
signature S =
   sig
      type ('a, 'b, 'k) v
      type ('a, 'b, 'k) u = ('a, 'b, 'k) v -> 'k

      val $ : ('a, 'b, 'b) v
      val fold: 'b * ('a * 'b -> 'b) -> ('a, 'b, 'k) u
      val a: ('a, 'b, 'a -> ('a, 'b, 'k) u) v
   end

functor F (S: S) =
   struct
      open S

      fun p i = print (concat [Int.toString i, "\n"])
      val () = p (fold (0, op +) a 1 a 2 a 3 $)
   end
  
structure S: S =
   struct
      type ('a, 'b, 'k) v = 'b * ('a * 'b -> 'b) -> 'k
      type ('a, 'b, 'k) u = ('a, 'b, 'k) v -> 'k

      fun $ (b, _) = b
      fun fold z v = v z
      fun a (b, f) x v = v (f (x, b), f)
   end

structure Z = F (S)
----------------------------------------------------------------------

This even generalizes well to the case where "a" takes multiple
arguments.

> I just remembered the specific reason why I liked the notation using
> lists better than the notation using composition. [] provides a natural
> way of referring to the defaults (empty set of updates to the defaults).
> The function application/CPS technique also allows you to express the
> empty set of updates naturally, but the composition technique needs an
> extra symbol.
> 
>   CPS:   f $     f (a) $     f (a) (b) $
>   o/&:   f id    f (a)       f (a & b)
>    []:   f []    f [a]       f [a, b]
> 
> I find the list notation the nicest. Too bad it has a performance
> disadvantage.

I don't find "id" all that bad.  Anyways, I don't find the syntactic
differences between these to be significant enough to outweigh other
concerns.  The performance disadvantage of lists rules it out for me.
In the absence of other factors I prefer the CPS because of the use of
infix "&" (or even worse "o") in the composition approach.

With the hack above, the CPS row looks like

    CPS:   f $     f a $       f a b $

Although, it probably makes more sense to consider that each argument
as two components, the "attribute" and the "value".  So things really
look like:

    CPS:   f $     f a X $     f a X b Y $
    o/&:   f id    f (a X)     f (a X & b Y)
     []:   f []    f [a X]     f [a X, b Y]

The CPS approach will need to paren the X and Y only if they are not
atomic expressions.  This tilts the syntax slightly more toward the
CPS approach in my opinion.

More substantively, both the continuation approach and the
composition-of-arguments have an advantage over the list approach: all
of the arguments need not be of the same type!

In the list approach, "a X" and "b Y" must have the same type.  In the
composition approach, we only require that they compose to the
appropriate type.  I'm not clear if this generality is useful, but it
may be.  In the CPS approach, we have even more flexibility, since we
can play tricks with the continuation.  For example, we could require
a strict alternation between two different kinds of arguments.

----------------------------------------------------------------------
signature S =
   sig
      type 'k v1
      type 'k v2

      val $ : int v1
      val f: 'k v1 -> 'k
      val a: (int -> 'k v2 -> 'k) v1
      val b: (real -> 'k v1 -> 'k) v2
   end

functor F (S: S) =
   struct
      open S

      fun p i = print (concat [Int.toString i, "\n"])
      val () = p (f $)
(*      val () = p (f a 1 $) *)  (* type error *)
      val () = p (f a 1 b 2.6 $)
   end

structure S: S =
   struct
      type 'k v = int -> 'k
      type 'k v1 = 'k v
      type 'k v2 = 'k v

      fun $ i = i
      fun f kv = kv 0
      fun a i j kv = kv (i + j)
      fun b i x kv = kv (i + round x)
   end

structure Z = F (S)
----------------------------------------------------------------------

This is clearly not possible with the list approach; I'm not sure
whether it is possible with the composition approach.

The CPS approach also generalizes nicely to other situations like
Printf.  Again, the list approach surely does not and I'm not sure if
the composition approach can be made to work there.