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.