MLton

This page provides a gentle introduction and derivation of Printf, with sections and arrangement more suitable to a talk.

Introduction

SML does not have printf. Could we define it ourselves?

val () = printf ("here's an int %d and a real %f.\n", 13, 17.0)
val () = printf ("here's three values (%d, %f, %f).\n", 13, 17.0, 19.0)

What could the type of printf be?

This obviously can’t work, because SML functions take a fixed number of arguments. Actually they take one argument, but if that’s a tuple, it can only have a fixed number of components.

From tupling to currying

What about currying to get around the typing problem?

val () = printf "here's an int %d and a real %f.\n" 13 17.0
val () = printf "here's three values (%d, %f, %f).\n" 13 17.0 19.0

That fails for a similar reason. We need two types for printf.

val printf: string -> int -> real -> unit
val printf: string -> int -> real -> real -> unit

This can’t work, because printf can only have one type. SML doesn’t support programmer-defined overloading.

Overloading and dependent types

Even without worrying about number of arguments, there is another problem. The type of printf depends on the format string.

val () = printf "here's an int %d and a real %f.\n" 13 17.0
val () = printf "here's a real %f and an int %d.\n" 17.0 13

Now we need

val printf: string -> int -> real -> unit
val printf: string -> real -> int -> unit

Again, this can’t possibly working because SML doesn’t have overloading, and types can’t depend on values.

Idea: express type information in the format string

If we express type information in the format string, then different uses of printf can have different types.

type 'a t  (* the type of format strings *)
val printf: 'a t -> 'a
infix D F
val fs1: (int -> real -> unit) t = "here's an int "D" and a real "F".\n"
val fs2: (int -> real -> real -> unit) t =
   "here's three values ("D", "F", "F").\n"
val () = printf fs1 13 17.0
val () = printf fs2 13 17.0 19.0

Now, our two calls to printf type check, because the format string specializes printf to the appropriate type.

The types of format characters

What should the type of format characters D and F be? Each format character requires an additional argument of the appropriate type to be supplied to printf.

Idea: guess the final type that will be needed for printf the format string and verify it with each format character.

type ('a, 'b) t   (* 'a = rest of type to verify, 'b = final type *)
val ` : string -> ('a, 'a) t  (* guess the type, which must be verified *)
val D: (int -> 'a, 'b) t * string -> ('a, 'b) t  (* consume an int *)
val F: (real -> 'a, 'b) t * string -> ('a, 'b) t  (* consume a real *)
val printf: (unit, 'a) t -> 'a

Don’t worry. In the end, type inference will guess and verify for us.

Understanding guess and verify

Now, let’s build up a format string and a specialized printf.

infix D F
val f0 = `"here's an int "
val f1 = f0 D " and a real "
val f2 = f1 F ".\n"
val p = printf f2

These definitions yield the following types.

val f0: (int -> real -> unit, int -> real -> unit) t
val f1: (real -> unit, int -> real -> unit) t
val f2: (unit, int -> real -> unit) t
val p: int -> real -> unit

So, p is a specialized printf function. We could use it as follows

val () = p 13 17.0
val () = p 14 19.0

Type checking this using a functor

signature PRINTF =
   sig
      type ('a, 'b) t
      val ` : string -> ('a, 'a) t
      val D: (int -> 'a, 'b) t * string -> ('a, 'b) t
      val F: (real -> 'a, 'b) t * string -> ('a, 'b) t
      val printf: (unit, 'a) t -> 'a
   end

functor Test (P: PRINTF) =
   struct
      open P
      infix D F

      val () = printf (`"here's an int "D" and a real "F".\n") 13 17.0
      val () = printf (`"here's three values ("D", "F ", "F").\n") 13 17.0 19.0
   end

Implementing Printf

Think of a format character as a formatter transformer. It takes the formatter for the part of the format string before it and transforms it into a new formatter that first does the left hand bit, then does its bit, then continues on with the rest of the format string.

structure Printf: PRINTF =
   struct
      datatype ('a, 'b) t = T of (unit -> 'a) -> 'b

      fun printf (T f) = f (fn () => ())

      fun ` s = T (fn a => (print s; a ()))

      fun D (T f, s) =
         T (fn g => f (fn () => fn i =>
                       (print (Int.toString i); print s; g ())))

      fun F (T f, s) =
         T (fn g => f (fn () => fn i =>
                       (print (Real.toString i); print s; g ())))
   end

Testing printf

structure Z = Test (Printf)

User-definable formats

The definition of the format characters is pretty much the same. Within the Printf structure we can define a format character generator.

val newFormat: ('a -> string) -> ('a -> 'b, 'c) t * string -> ('b, 'c) t =
   fn toString => fn (T f, s) =>
   T (fn th => f (fn () => fn a => (print (toString a); print s ; th ())))
val D = fn z => newFormat Int.toString z
val F = fn z => newFormat Real.toString z

A core Printf

We can now have a very small PRINTF signature, and define all the format strings externally to the core module.

signature PRINTF =
   sig
      type ('a, 'b) t
      val ` : string -> ('a, 'a) t
      val newFormat: ('a -> string) -> ('a -> 'b, 'c) t * string -> ('b, 'c) t
      val printf: (unit, 'a) t -> 'a
   end

structure Printf: PRINTF =
   struct
      datatype ('a, 'b) t = T of (unit -> 'a) -> 'b

      fun printf (T f) = f (fn () => ())

      fun ` s = T (fn a => (print s; a ()))

      fun newFormat toString (T f, s) =
         T (fn th =>
            f (fn () => fn a =>
               (print (toString a)
                ; print s
                ; th ())))
   end

Extending to fprintf

One can implement fprintf by threading the outstream through all the transformers.

signature PRINTF =
   sig
      type ('a, 'b) t
      val ` : string -> ('a, 'a) t
      val fprintf: (unit, 'a) t * TextIO.outstream -> 'a
      val newFormat: ('a -> string) -> ('a -> 'b, 'c) t * string -> ('b, 'c) t
      val printf: (unit, 'a) t -> 'a
   end

structure Printf: PRINTF =
   struct
      type out = TextIO.outstream
      val output = TextIO.output

      datatype ('a, 'b) t = T of (out -> 'a) -> out -> 'b

      fun fprintf (T f, out) = f (fn _ => ()) out

      fun printf t = fprintf (t, TextIO.stdOut)

      fun ` s = T (fn a => fn out => (output (out, s); a out))

      fun newFormat toString (T f, s) =
         T (fn g =>
            f (fn out => fn a =>
               (output (out, toString a)
                ; output (out, s)
                ; g out)))
   end

Notes

  • Lesson: instead of using dependent types for a function, express the the dependency in the type of the argument.

  • If printf is partially applied, it will do the printing then and there. Perhaps this could be fixed with some kind of terminator.

    A syntactic or argument terminator is not necessary. A formatter can either be eager (as above) or lazy (as below). A lazy formatter accumulates enough state to print the entire string. The simplest lazy formatter concatenates the strings as they become available:

    structure PrintfLazyConcat: PRINTF =
       struct
          datatype ('a, 'b) t = T of (string -> 'a) -> string -> 'b
    
          fun printf (T f) = f print ""
    
          fun ` s = T (fn th => fn s' => th (s' ^ s))
    
          fun newFormat toString (T f, s) =
             T (fn th =>
                f (fn s' => fn a =>
                   th (s' ^ toString a ^ s)))
       end

    It is somewhat more efficient to accumulate the strings as a list:

    structure PrintfLazyList: PRINTF =
       struct
          datatype ('a, 'b) t = T of (string list -> 'a) -> string list -> 'b
    
          fun printf (T f) = f (List.app print o List.rev) []
    
          fun ` s = T (fn th => fn ss => th (s::ss))
    
          fun newFormat toString (T f, s) =
             T (fn th =>
                f (fn ss => fn a =>
                   th (s::toString a::ss)))
       end

Also see