The printf-like functions are variadic, in the sense that they accept a variable number of arguments. It is not really specific to printf and the family, you can define your own variadic functions in OCaml, this is fully supported by the type system. The only magic of printf is that the compiler translates a string literal, e.g., "foo %d to a value of type format.
Now, let's look at the type of the printf function,
('a, out_channel, unit) format -> 'a
Notice that it returns 'a which is a type variable. Since 'a could be anything it could be also a function. The ('a, out_channel, unit) format is the type of the format string that defines the type of function that is generated by this format string. It is important to understand though, that despite that "foo %d" looks like a string, in fact, it is a special built-in value of type _ format, which has a literal that looks like a string (though not all valid strings are valid literals for the _ format type.
Just to demonstrate that the first argument of printf is not a string, let's try the following,
# Printf.printf ("foo " ^ "%d");;
Line 1, characters 14-29:
1 | Printf.printf ("foo " ^ "%d");;
^^^^^^^^^^^^^^^
Error: This expression has type string but an expression was expected of type
('a, out_channel, unit) format
Now, when we know that printf is not a typical function, let's define a printf-like function ourselves. For that we need to use the kprintf-family of functions, e.g.,
# #show Printf.ksprintf;;
val ksprintf : (string -> 'd) -> ('a, unit, string, 'd) format4 -> 'a
This function takes the function which receives the resulting string which we can log, for example,
# let log fmt = Printf.ksprintf (fun s -> print_endline ("log> "^s)) fmt;;
val log : ('a, unit, string, unit) format4 -> 'a = <fun>
# log "foo";;
log> foo
- : unit = ()
This resulting function looks more like sprintf, i.e., it will play nicely with pretty-printing function that work with string as their output devices (this is a different topic). You may find it more easier to define your logging functions, using either Printf.kfprintf or, much better, using Format.kasprintf or Format.kfprintf. The latter two functions have the following types,
val kasprintf : (string -> 'a) -> ('b, formatter, unit, 'a) format4 -> 'b
val kfprintf : (formatter -> 'a) -> formatter ->
('b, formatter, unit, 'a) format4 -> 'b
But the type of format works with the formatter type (which is an abstraction of the output device) that is the type that the pretty printers (conventionally named pp) are accepting. So the log function defined using the Format module will play better with the existing libraries.
So, using Format.kasprintf we can define your log function as,
# let log verbosity level =
Format.kasprintf (fun msg ->
if level <= verbosity then
Format.printf "Level %d: %s@\n%!" level msg);;
val log : int -> int -> ('a, Format.formatter, unit, unit) format4 -> 'a = <fun>
And here is how it could be used,
# log 0 0 "Hello, %s, %d times" "world" 3;;
Level 0: Hello, world, 3 times
- : unit = ()