It's mainly related to the basics of the type system, but sometimes related to other considerations on the philosophy of the language.
print with arbitrary values
Some languages have a
print function that accepts arbitrary values. Others need the program to explicitly convert data to a string. What makes the difference is whether the language makes it possible to infer how to convert the data with some default conversion mechanism (e.g. integers printed in decimal, lists printed as something like the language's list constructor syntax, etc.).
In a dynamically typed language like Python, it's easy. All values have runtime type information, so the printing function can use that to determine how to print the argument.
In statically typed languages, there typically isn't enough information at runtime to find a sensible way to convert a value's representation to a string. For example, a word in memory could contain an integer or a float or a pointer and there's no way to find out which. So any automatic conversion to string has to follow some rules at compile time, where the compiler generates the correct formatting function based on the argument's static type.
C has no such mechanism. C is a low-level language which gives the programmer a lot of control. If you want to print an integer, you have to specify whether you want it in decimal, hexadecimal or octal. If you want to print a floating-point value, you have to specify how many digits of precision. This philosophy is prevalent in C, so the design of the language doesn't include any mechanism for selecting between different implementations of a function based on the argument type. (From C11 onwards there is such a mechanism, but it's restricted to selecting between floating point types for a few built-in functions.)
Many statically typed languages have an overloading mechanism that allows the selection of different implementations of a function based on the argument's type. For example, in C++, the
<< operator selects between different printing functions based on the type of its right-hand argument, so you can write
cout << 1,
cout << 1.5,
cout << "hello", etc. (And
<< even selects between being a printing function and a bitwise operator, based on the type of its left-hand argument.) This is a compile-time mechanism: the compiler knows that there are many
<< functions, each with their type. In Haskell, this is the
Show type class, with an instance for each type or type constructor for which a printing mechanism exists.
printf with a template
Almost all general-purpose languages have function that's similar to C's
sprintf, often with a similar template syntax. They may or may not have a function that's similar to
printf, combining the string templating with printing: when combining a string-printing function with a template formatting function is simple enough, there's no need for a function that does both.
For example, in Python, the equivalent of C's
sprintf is the
% operator, which uses a printf-like template syntax. This templating mechanism from the original version of the language is somewhat deprecated in modern Python in favor of a different template syntax that is accessible via the
format method on the template string; the syntax is different, but the core principle is the same. Python only has a
sprintf equivalent, not a
printf equivalent, because there wouldn't be much point: it's almost as easy to write
print(template % (arg1, arg2)) or
print(template.format(arg1, arg2)) as it would be to write
print(template, arg1, arg2).
Having separate functions requires constructing the resulting string in memory. In C, that's a big deal: you have to allocate enough memory, and C gives you a choice of allocators: you can use a global buffer, or a buffer on the stack, or a buffer allocated by
malloc, or a buffer allocated by some custom allocator. So it's convenient to have a function that can directly print out the data piece by piece. In a high-level language like Python, the allocation is done entirely under the hood and allocating a string in memory is not considered a big deal.
The main reason some general-purpose don't have a
sprintf-like function is that it's hard to fit into a static type system. You have to match the content of the template string with the types of the other arguments, and even with the number of other arguments. In C, that's not a problem because the type system is very far from sound. It's the responsibility of the programmer to pass the correct number of arguments with the correct types. In dynamically typed languages, that's not a problem because the templating function can check argument types at runtime.
Many statically typed languages arrange some printf-like mechanism anyway, with various degree of “compiler magic”, i.e. you couldn't implement
printf as a library function. For example, Pascal has special handling for some functions like
write which accept multiple argument types and even a field width formatting syntax. You can write
write('x=', x:3) but you can't make your own function that accepts a variable number of arguments, or that takes the extra width annotation
:3. In Ocaml, the
printf mechanism requires the template to be a string literal, which the compiler parses to deduce the expected type of the argument list:
printf itself has a special type for which you can't build values normally, but
sprintf "x=%3" has the type
int -> string.
Haskell's type classes (a very fancy overloading mechanism) are powerful enough to define a
printf function in the base library, but it requires runtime validation of the template against its type. The types of the arguments determine the type class of the template, which determines the implementation of the template-to-string conversion. The template-to-string conversion uses the template string to determine exactly what must be printed, but needs to validate that it matches the expected types for the arguments. For example,
printf template 1 results in code that knows that it needs to format one integer, and will accept
x=%3d as a template but not