OCaml from the ground up

Text, and lines of formatted output

Joining, slicing and measuring strings, turning numbers into text and back, and Printf.printf with the s, d and f conversions.

Part 10 of 12

Advanced20 min readBack to Courses

Strings are immutable

A string is a fixed sequence of bytes: the caret builds a new one from two, String.length counts them, and String.sub s start len copies a piece out. Nothing edits a string in place, so a function that seems to change one answers a new string instead.

String.concat ", " joins a list of strings with a separator, which is how a list of numbers becomes one printable line once every number has gone through string_of_int. The conversion functions are the bridge between the typed world and the world of text: string_of_int, string_of_float, and int_of_string going the other way.

A template instead of a pile of carets

Printf.printf fills the gaps in a template: %s for a string, %d for an int and %f for a float. The format string is type-checked against the arguments, so a %d handed a string is a compile error rather than a strange line of output.

%f on its own writes six decimal places; %.2f writes two, and a width such as %5d pads the number to a column. A newline is written as \n at the end of the template, which is why the printed lines end where you expect them to. Width and precision are part of the template, so a column’s alignment is decided once, where the template is written, rather than at every call.

Bytes, bounds, and the format string

A string is a sequence of bytes, not a sequence of visible characters. For ASCII text the two are the same, and this course stays there; for accented letters or emoji one character can be several bytes, so String.length counts bytes and String.sub can cut a character in half. That is worth knowing before slicing text that came from a person.

String.sub s start len requires start and len to describe a real piece of the string. If start is negative or start + len is past the end, it raises Invalid_argument rather than returning a shorter string. Check the length first, or clamp the count with min, which is what the snippet below does.

Printf.printf checks its template at compile time. %s wants a string, %d wants an int, %f wants a float, and the arguments are consumed left to right. A width such as %5d or %-10s only changes the spaces; it does not truncate a value that is longer than the field.

How the caret builds a string

^ allocates a new string and copies both sides into it. It is not a rope or a view: "a" ^ "b" has length two and its own bytes, and neither argument is changed. That is why joining a list with String.concat beats folding with ^ — the library adds up the total length once and copies each piece once, while a left fold copies the growing prefix again on every step.

For a handful of pieces the difference does not matter. For a list of a thousand lines it is quadratic work hidden in a one-character operator, which is worth knowing before a log builder starts to crawl.

What goes wrong

int_of_string is the first: it raises Failure "int_of_string" on "12px", "3.5" and the empty string. It does not parse a prefix and it does not round; if the text may be malformed, use int_of_string_opt and handle None with the option tools from the last chapter.

The second is a format string that compiles but prints the wrong thing. %d on a float is a type error, so the compiler catches the gross mismatch; what it cannot catch is %d on an int you meant to show as 3.00, or %s on a string that needed a width. Choose the conversion from the value’s type and the reader’s need, not from habit.

Three output mistakes: %d with a float, which is a type error rather than a rounded number; String.sub with a length that runs past the end, which raises Invalid_argument at run time; and a missing \n at the end of a printf template, which leaves the next line attached to this one. The escape is written inside the string, not after it.

Try these next

  • Print the first and last two letters of a word with String.sub, checking the length first.
  • Format three rows with %-8s and %6.2f, then remove the minus and see what moves.
  • Join a list of three words with String.concat and print the result.

Slicing, and a formatted line

let shout text =
  String.uppercase_ascii (String.sub text 0 (min 3 (String.length text)))

let () =
  Printf.printf "%s! %d letters\n" (shout "ocaml") (String.length "ocaml")

Length, slice, join, format

Run it and read the nine lines. Then print the last three letters of `ocaml`, and change the printf line so the name comes after the count.

Output
Click "Run" to execute your code

Initials, a slice and a table

Run it and read the lines. Then print the last four letters of the word, and add a third row to the price table with a name longer than eight characters.

Output
Click "Run" to execute your code

int_of_string raises a Failure when the text is not a number, so a program that reads it either catches that or accepts that a malformed value stops the run. It never answers a wrong number, which is the part worth relying on.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is String.sub "ocaml" 0 3?