MLton 20051202 Printf
Home  Index  
Programmers coming from C or Java often ask if Standard ML has a printf function. It does not. However, it is possible to write your own. In practice, however, it is not so important to do so - it is much more common to use a style in which you convert values to strings and concatenate to form the final the string, as in
val () = print (concat ["var = ", Int.toString var, "\n"])

Here is the signature for a printf function with user definable formats (defined by newFormat).

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

A structure matching PRINTF could be used as follows.

functor TestPrintf (S: PRINTF) =
   struct
      open S

      (* define some formats (the names are mnemonics of C's %c %d %s %f) *)
      fun C z = newFormat Char.toString z
      fun D z = newFormat Int.toString z
      fun S z = newFormat (fn s => s) z
      fun F z = newFormat Real.toString z

      infix C F D S

      val () = printf (`"here's a string "S" and an int "D".\n") "foo" 13
      val () = printf (`"here's a char "C".\n") #"c"
      val () = printf (`"here's a real "F".\n") 13.0
   end

With no special compiler support, SML's type system ensures that the format characters (C, D, F, S) are supplied the correct type of argument. Try modifying the above code to see what error you get if you pass the wrong type.

The real trick is in implementing PRINTF. Here is an implementation based on Functional Unparsing.

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

      type ('a, 'b) t = (out -> 'a) -> (out -> 'b)

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

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

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

      fun newFormat f (a, b) k =
         a (fn out => fn s =>
            (output (out, f s)
             ; output (out, b)
             ; k out))
   end

To make a complete program and test the above code, we can apply the TestPrintf functor to our implementation.

structure S = TestPrintf (Printf)

Running the complete code prints out the following.

here's a string foo and an int 13.
here's a char c.
here's a real 13.

Efficiency

printf is rarely a bottleneck in programs. However, you may be curious how the above implementation performs compared with the string-based C one. Fortunately, MLton's aggressive optimization inlines away all the wrapper functions, leaving only the coercions interspersed with calls to print. Thus, with MLton, the processing of the format characters occurs at compile time, which should be even faster than C's approach of processing the format characters at run time.

For example, MLton expands the above program into something like the following.

(print "here's a string "
 ; print "foo"
 ; print " and an int "
 ; print (Int.toString 13)
 ; print ".\n"
 ; print "here's a char "
 ; print (Char.toString #"c")
 ; print ".\n"
 ; print "here's a real "
 ; print (Real.toString 13.0)
 ; print ".\n")

If you're fluent in MLton's intermediate languages, you can compile the program with -keep-pass polyvariance and look at the IL to confirm this.

Download

Also see


Last edited on 2005-01-30 23:56:46 by MatthewFluet.