2.6. Printing#
OCaml has built-in printing functions for a few of the built-in primitive
types: print_char
, print_string
, print_int
, and print_float
. There’s
also a print_endline
function, which is like print_string
, but also outputs
a newline.
print_endline "Camels are bae"
Camels are bae
- : unit = ()
2.6.1. Unit#
Let’s look at the types of a couple of those functions:
print_endline
- : string -> unit = <fun>
print_string
- : string -> unit = <fun>
They both take a string as input and return a value of type unit
, which we
haven’t seen before. There is only one value of this type, which is written ()
and is also pronounced “unit”. So unit
is like bool
, except there is one
fewer value of type unit
than there is of bool
.
Unit is used when you need to take an argument or return a value, but there’s no
interesting value to pass or return. It is the equivalent of void
in Java, and
is similar to None
in Python. Unit is often used when you’re writing or using
code that has side effects. Printing is an example of a side effect: it changes
the world and can’t be undone.
2.6.2. Semicolon#
If you want to print one thing after another, you could sequence some print
functions using nested let
expressions:
let _ = print_endline "Camels" in
let _ = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()
The let _ = e
syntax above is a way of evaluating e
but not binding
its value to any name. Indeed, we know the value each of those print_endline
functions will return: it will always be ()
, the unit value. So there’s
no good reason to bind it to a variable name. We could also write let () = e
to indicate we know it’s just a unit value that we don’t care about:
let () = print_endline "Camels" in
let () = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()
But either way the boilerplate of all the let..in
is annoying to have to
write! So there’s a special syntax that can be used to chain
together multiple functions that return unit. The expression e1; e2
first
evaluates e1
, which should evaluate to ()
, then discards that value, and
evaluates e2
. So we could rewrite the above code as:
print_endline "Camels";
print_endline "are";
print_endline "bae"
Camels
are
bae
- : unit = ()
That is more idiomatic OCaml code, and it also looks more natural to imperative programmers.
Warning
There is no semicolon after the final print_endline
in that example. A common
mistake is to put a semicolon after each print statement. Instead, the
semicolons go strictly between statements. That is, semicolon is a statement
separator not a statement terminator. If you were to add a semicolon at the
end, you could get a syntax error depending on the surrounding code.
2.6.3. Ignore#
If e1
does not have type unit
, then e1; e2
will give a warning, because
you are discarding a potentially useful value. If that is truly your intent, you
can call the built-in function ignore : 'a -> unit
to convert any value to
()
:
(ignore 3); 5
- : int = 5
Actually ignore
is easy to implement yourself:
let ignore x = ()
val ignore : 'a -> unit = <fun>
Or you can even write underscore to indicate the function takes in a value but does not bind that value to a name. That means the function can never use that value in its body. But that’s okay: we want to ignore it.
let ignore _ = ()
val ignore : 'a -> unit = <fun>
2.6.4. Printf#
For complicated text outputs, using the built-in functions for primitive type printing quickly becomes tedious. For example, suppose you wanted to write a function to print a statistic:
(** [print_stat name num] prints [name: num]. *)
let print_stat name num =
print_string name;
print_string ": ";
print_float num;
print_newline ()
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()
How could we shorten print_stat
? In Java you might use the overloaded +
operator to turn all objects into strings:
void print_stat(String name, double num) {
System.out.println(name + ": " + num);
}
But OCaml values are not objects, and they do not have a toString()
method
they inherit from some root Object
class. Nor does OCaml permit overloading of
operators.
Long ago though, FORTRAN invented a different solution that other languages like
C and Java and even Python support. The idea is to use a format specifier to
—as the name suggest— specify how to format output. The name this
idea is best known under is probably “printf”, which refers to the name of the C
library function that implemented it. Many other languages and libraries still
use that name, including OCaml’s Printf
module.
Here’s how we’d use printf
to re-implement print_stat
:
let print_stat name num =
Printf.printf "%s: %F\n%!" name num
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()
The first argument to function Printf.printf
is the format specifier. It
looks like a string, but there’s more to it than that. It’s actually
understood by the OCaml compiler in quite a deep way. Inside the format
specifier there are:
plain characters, and
conversion specifiers, which begin with
%
.
There are about two dozen conversion specifiers available, which you can read
about in the documentation of Printf
. Let’s pick apart the
format specifier above as an example.
It starts with
"%s"
, which is the conversion specifier for strings. That means the next argument toprintf
must be astring
, and the contents of that string will be output.It continues with
": "
, which are just plain characters. Those are inserted into the output.It then has another conversion specifier,
%F
. That means the next argument ofprintf
must have typefloat
, and will be output in the same format that OCaml uses to print floats.The newline
"\n"
after that is another plain character sequence.Finally, the conversion specifier
"%!"
means to flush the output buffer. As you might have learned in earlier programming classes, output is often buffered, meaning that it doesn’t all happen at once or right away. Flushing the buffer ensures that anything still sitting in the buffer gets output immediately. This specifier is special in that it doesn’t actually need another argument toprintf
.
If the type of an argument is incorrect with respect to the conversion specifier,
OCaml will detect that. Let’s add a type annotation to force num
to be an
int
, and see what happens with the float conversion specifier %F
:
let print_stat name (num : int) =
Printf.printf "%s: %F\n%!" name num
File "[14]", line 2, characters 34-37:
2 | Printf.printf "%s: %F\n%!" name num
^^^
Error: This expression has type int but an expression was expected of type
float
To fix that, we can change to the conversion specifier for int
, which is %i
:
let print_stat name num =
Printf.printf "%s: %i\n%!" name num
val print_stat : string -> int -> unit = <fun>
Another very useful variant of printf
is sprintf
, which collects the output
in string instead of printing it:
let string_of_stat name num =
Printf.sprintf "%s: %F" name num
val string_of_stat : string -> float -> string = <fun>
string_of_stat "mean" 84.39
- : string = "mean: 84.39"