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

- printf.sml

## Also see

- PrintfGentle

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