[MLton] printf with no infixes

Vesa Karvonen vesa.karvonen@cs.helsinki.fi
Thu, 25 Aug 2005 15:26:45 +0300


Quoting Stephen Weeks <sweeks@sweeks.com>:
> The latest varargs hack that I sent yesterday can be used to improve
> printf so that no infix operators are needed.
[...]
>      val () = printf "Hello.\n"$
>      val () = printf "An int "D" and an int "D".\n"$ 13 14
>      val () = printf "An int "D" and a real "G".\n"$ 13 3.1415
>      val () = printf "A real "G" and a real "G".\n"$ 13.1 3.1415

Wow, that is very nice! The elimination of infix operators makes the
technique a whole lot more practical in my opinion.

However, I must say that the above (and the original) syntax has one
characteristic that I dislike. The issue is that you can't have two
consecutive conversion specifiers nor two consecutive literal strings. I'm
not saying that it would be a common need; the above syntax probably does
90% (or more) of the cases without surprise.

Consider the following regular expression for the above printf syntax

  printf (<string> <conversion-specifier>)* <string> $ <arg>*

and contrast it with the following alternative printf syntax

  printf (`<string> | <conversion-specifier> <arg>*)* $ <arg>*

The alternative syntax allows an empty formatting sequence (meaning the
stuff between printf and $) and arbitrary number of consecutive literal
strings (prefixed by `) or conversion specifiers. IMO, this makes the
syntax considerably more regular and probably reduces surprises. As you
can see, it also allows you to conveniently pass arguments (without having
to use parentheses) to a conversion specifier within the formatting
sequence. You could even have a conversion specifier that takes a variable
number of arguments. The idea is also very simple: the continuation of the
formatting sequence is always determined by the immediately following
specifier (` or something else). Below is a quick implementation of the
alternative syntax.

  fun id x = x

  fun printf f = f id
  fun $ m = m (fn ss => List.app print (rev ss)) []
  fun ` m s f = f (fn k => m (fn ss => k (s::ss)))

  fun newFormat toS m f = f (fn k => m (fn ss => fn x => k (toS x::ss)))

  val C = fn z => newFormat Char.toString z
  val D = fn z => newFormat Int.toString z
  val G = fn z => newFormat Real.toString z
  val S = fn z => newFormat id z

Here is a translation of the original examples:

  val () = printf `"Hello.\n"$
  val () = printf `"An int "D`" and an int "D`".\n"$ 13 14
  val () = printf `"An int "D`" and a real "G`".\n"$ 13 3.1415
  val () = printf `"A real "G`" and a real "G`".\n"$ 13.1 3.1415

Here are examples that use the new flexibility:

  val () = printf $
  val () = printf `"A string" `" - followed by another.\n"$
  val () = printf G C `"\n"$ 1.0 #"f"

Here is an example (not an extremely ingenious one) of a conversion
specifier with an argument in the formatting sequence:

  fun newFormatWithArg toS m arg f =
      f (fn k => m (fn ss => fn x => k (toS (arg, x)::ss)))

  val DL = fn z => newFormatWithArg
                      (fn (n, i) =>
                          StringCvt.padRight #" " n (Int.toString i))
                      z

  val () = printf `"A padded int '"DL 5`"'.\n"$ 12

-Vesa Karvonen