OCaml Module 2 – Expressions, values and functions
Booleans, characters, strings and bytes
Compare values with = and ==, see why OCaml strings are immutable bytes, count UTF-8 characters correctly, and use Bytes, Buffer and the new String helpers.
What you will learn
- Use bool operators, char and string literals, escapes and the String module
- Explain why strings are immutable byte sequences and when to use Bytes or Buffer
- Handle UTF-8 text correctly with String.get_utf_8_uchar and Uchar
- Use the OCaml 5.4 and 5.5 string helpers such as split_first, replace_all and edit_distance
Before you start
What you’ll learn · Before you start
What you will learn
- Use bool operators, char and string literals, escapes and the String module
- Explain why strings are immutable byte sequences and when to use Bytes or Buffer
- Handle UTF-8 text correctly with String.get_utf_8_uchar and Uchar
- Use the OCaml 5.4 and 5.5 string helpers such as split_first, replace_all and edit_distance
Before you start
On this page
String.length "नमस्ते" is 18 in OCaml 5.5.1, though the Hindi greeting has six characters. Nothing is wrong: an
OCaml string is a sequence of bytes, and in UTF-8 each of these characters takes three.
You will use booleans, characters and string literals, compare values with = and ==, see why strings cannot be
changed and when Bytes or Buffer fit instead, count characters in UTF-8 text correctly, and use the String helpers
that OCaml 5.4 and 5.5 added.
Booleans short-circuit, and the two equalities ask different questions
bool has two values, true and false. && and || evaluate their right side only when they need it, so
f x && g x never calls g when f x is false, and not is an ordinary function. A char is written in single
quotes, 'a', and a string in double quotes, with escapes such as \n and \t. The toplevel shows a string with its
escapes; printing it shows the characters:
from session import toplevel
# Booleans short-circuit, = compares structure and == compares identity, compare orders strings byte by byte, chars
# and strings have their own literals, and a string cannot be changed in place.
toplevel("""
true && false || not false;;
false && (print_endline "right side ran"; true);;
true && (print_endline "right side ran"; true);;
"abc" = "abc";;
"abc" == "abc";;
List.sort compare ["mango"; "Zebra"; "apple"];;
'a';;
Char.code 'a';;
"tab:\\there";;
print_endline "tab:\\there";;
"abc".[0];;
"abc".[0] <- 'x';;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# true && false || not false;;
- : bool = true
# false && (print_endline "right side ran"; true);;
- : bool = false
# true && (print_endline "right side ran"; true);;
right side ran
- : bool = true
# "abc" = "abc";;
- : bool = true
# "abc" == "abc";;
- : bool = false
# List.sort compare ["mango"; "Zebra"; "apple"];;
- : string list = ["Zebra"; "apple"; "mango"]
# 'a';;
- : char = 'a'
# Char.code 'a';;
- : int = 97
# "tab:\there";;
- : string = "tab:\there"
# print_endline "tab:\there";;
tab: here
- : unit = ()
# "abc".[0];;
- : char = 'a'
# "abc".[0] <- 'x';;
Line 1, characters 0-16:
1 | "abc".[0] <- 'x';;
^^^^^^^^^^^^^^^^
Error: Syntax error: strings are immutable, there is no assignment syntax for
them.
Hint: Mutable sequences of bytes are available in the Bytes module.
Hint: Did you mean to use Bytes.set?
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 basics_session.py
The two equalities differ. = is structural equality: Comparing two values by their contents with =, so two separately built strings with the same characters are equal. == compares identity instead.: it compares contents,
so two strings that spell abc are equal. == is physical equality: it asks whether both sides are the very same
value in memory, and two separately written strings are not, so "abc" == "abc" was false. Compare text and data
with =; == is for the rare code that cares about identity.
The second and third phrases show the short circuit at work. In false && (print_endline "right side ran"; true) the
left side already decided the answer, so the right side never ran and nothing was printed; with true on the left,
the right side had to run and printed its line. Order the tests of a condition so that the cheap or protective one
comes first, as in i < String.length s && s.[i] = ' '.
Ordering is a third question, answered by compare, which returns a negative number, 0 or a positive number. On
strings it compares byte by byte, so List.sort compare ["mango"; "Zebra"; "apple"] gave
["Zebra"; "apple"; "mango"]: the byte of Z, 90, is smaller than the byte of a, 97, so every capital letter sorts
before every small one. A list shown to people needs a key such as String.lowercase_ascii first.
Char.code 'a' gave 97, the character’s byte value, and "abc".[0] read the first byte, 'a'. The last phrase
tried to change it, and the compiler refused with a hint that leads to the next section.
Using == to compare strings
Wrong: if input == "yes" then …, which is false even when the user typed yes, because the two strings are
different values. Right: if input = "yes" then …, or String.equal input "yes".
Strings are immutable bytes; Bytes and Buffer are for change
"abc".[0] <- 'x' was a syntax error: “strings are immutable, there is no assignment syntax for them”, with the hint
that mutable sequences of bytes are in the Bytes module. Immutability makes strings safe to share: no code anywhere
can change a string that another part of the program holds. When text really must change, Bytes holds bytes that
can be set in place, and Buffer builds text that grows:
from session import toplevel
# Bytes is the mutable sibling of string; Buffer grows a string piece by piece without copying it each time.
toplevel("""
let b = Bytes.of_string "hello";;
Bytes.set b 0 'J';;
Bytes.to_string b;;
let buf = Buffer.create 16;;
List.iter (fun field -> Buffer.add_string buf field; Buffer.add_char buf ',') ["name"; "city"; "pin"];;
Buffer.contents buf;;
Buffer.length buf;;
""") Output
$ ocaml OCaml version 5.5.1 Enter #help;; for help. # let b = Bytes.of_string "hello";; val b : bytes = Bytes.of_string "hello" # Bytes.set b 0 'J';; - : unit = () # Bytes.to_string b;; - : string = "Jello" # let buf = Buffer.create 16;; val buf : Buffer.t = <abstr> # List.iter (fun field -> Buffer.add_string buf field; Buffer.add_char buf ',') ["name"; "city"; "pin"];; - : unit = () # Buffer.contents buf;; - : string = "name,city,pin," # Buffer.length buf;; - : int = 14
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytes_buffer_session.py
Bytes.set b 0 'J' turned hello into Jello, and the Buffer collected three fields with commas, 14 bytes in all.
Building the same line with ^ would copy the whole string so far at every step, which is fine for two pieces and
slow for thousands; the Buffer module’s documentation promises concatenation in linear time, instead of the quadratic
time of joining strings pair by pair. Answer three questions about text your own code handles:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Type | Safe to share | Changed in place | Built from many pieces | Accepted by most functions |
|---|---|---|---|---|
| string What it is: immutable bytes, the usual type for text | Good nobody can change it under you | Poor no: s.[0] <- x is a syntax error | Poor ^ copies the whole string every time | Good what nearly every function takes and returns |
| Bytes.t What it is: mutable bytes of a fixed length | Poor every holder sees each Bytes.set | Good Bytes.set b 0 J made Jello | Poor its length is fixed when it is made | Mixed convert with Bytes.to_string to pass it on |
| Buffer.t What it is: bytes that grow as you add to them | Mixed share the finished Buffer.contents instead | Mixed appends, not edits in the middle | Good add_string and add_char: linear time for the whole text | Mixed Buffer.contents gives a string at the end |
How the answers decide
- “Will the text change after it has been built?” Yes, bytes are edited later: rules out string. A string can never be changed; edits in place need Bytes.
- “Is the text built from many pieces, for example in a loop?” Yes, many pieces: rules out string and Bytes.t. Joining with ^ copies everything built so far at each step, and Bytes cannot grow; Buffer appends without copying.
- “Will other parts of the program keep and read the text?” Yes, it is shared: adds a caveat to Bytes.t and Buffer.t. Code that holds the same Bytes sees every change; hand others a string, such as Bytes.to_string or Buffer.contents.
- “Will the text change after it has been built?” No, it is fixed once built and “Is the text built from many pieces, for example in a loop?” No, one or two: adds a caveat to Bytes.t and Buffer.t. For text that is fixed and short, a plain string is simpler and is what other functions expect.
Key idea
Keep text in strings, which nothing can change; reach for Bytes to edit bytes in place and Buffer to build long text, and hand a plain string on when you are done.
Checkpoint
Check: Booleans, chars and the two equalities · Immutable strings, Bytes and Buffer
A string’s length counts bytes, not characters
OCaml strings know nothing about character encodings: String.length counts bytes. UTF-8 stores ASCII characters in
one byte and others in two to four, so the Devanagari word took 18 bytes for 6 characters. To work with characters,
decode them. String.get_utf_8_uchar s i decodes the character that starts at byte i and reports how many bytes it
used, as the Uchar module documents:
from session import toplevel
# A string holds bytes. A word in Devanagari, and é in héllo, take several bytes per character; String.get_utf_8_uchar
# decodes one character and says how many bytes it took.
toplevel("""
let word = "नमस्ते";;
String.length word;;
let rec chars s i n = if i >= String.length s then n else chars s (i + Uchar.utf_decode_length (String.get_utf_8_uchar s i)) (n + 1);;
chars word 0 0;;
String.length "héllo";;
let d = String.get_utf_8_uchar "héllo" 1;;
Uchar.utf_decode_length d;;
Uchar.to_int (Uchar.utf_decode_uchar d);;
String.uppercase_ascii "héllo";;
""") Output
$ ocaml OCaml version 5.5.1 Enter #help;; for help. # let word = "नमस्ते";; val word : string = "नमस्ते" # String.length word;; - : int = 18 # let rec chars s i n = if i >= String.length s then n else chars s (i + Uchar.utf_decode_length (String.get_utf_8_uchar s i)) (n + 1);; val chars : String.t -> int -> int -> int = <fun> # chars word 0 0;; - : int = 6 # String.length "héllo";; - : int = 6 # let d = String.get_utf_8_uchar "héllo" 1;; val d : Uchar.utf_decode = <abstr> # Uchar.utf_decode_length d;; - : int = 2 # Uchar.to_int (Uchar.utf_decode_uchar d);; - : int = 233 # String.uppercase_ascii "héllo";; - : string = "HéLLO"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 utf8_session.py
The function chars stepped through the word by each character’s length and counted 6. In héllo, the decoder at
byte 1 found a character 2 bytes long, code 233, which is é, and the five-letter word had a length of 6:
| Bytes of héllo | 0 | 1 and 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| Character | h | é (U+00E9) | l | l | o |
String.uppercase_ascii "héllo" gave HéLLO: as its name says, it changes only ASCII letters. Code that cuts
strings at a byte position, such as String.sub s 0 2, can split a character in half, so count and cut by decoded
characters whenever text may not be plain English.
Text in Indian languages
Names and addresses in Devanagari, Tamil or Bengali script take three bytes per character in UTF-8, so a database limit of 50 bytes may hold only 16 characters. Validate lengths in characters, decoded as above, not in bytes.
OCaml 5.4 and 5.5 add helpers for everyday text
Splitting a line at a separator used to take index arithmetic. OCaml 5.5 adds functions for these jobs to the String
module, and 5.4 added String.edit_distance. The session applies them to lines of a key=value configuration file:
from session import toplevel
# Helpers that OCaml 5.5 added to String, used on lines of a key=value configuration file, and 5.4's edit_distance.
toplevel("""
String.split_first ~sep:"=" "timeout=30";;
String.split_first ~sep:"=" "no separator here";;
String.split_all ~sep:"," "a,b,,c";;
String.drop_first_while (fun c -> c = ' ') " port=8080";;
String.find_first_index (fun c -> c = '=') "port=8080";;
String.take_first 4 "port=8080";;
String.replace_all ~sub:"-" ~by:"_" "max-retry-count";;
String.includes ~affix:"key" "api_key=secret";;
String.is_empty "";;
String.edit_distance "kitten" "sitting";;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# String.split_first ~sep:"=" "timeout=30";;
- : (string * string) option = Some ("timeout", "30")
# String.split_first ~sep:"=" "no separator here";;
- : (string * string) option = None
# String.split_all ~sep:"," "a,b,,c";;
- : string list = ["a"; "b"; ""; "c"]
# String.drop_first_while (fun c -> c = ' ') " port=8080";;
- : string = "port=8080"
# String.find_first_index (fun c -> c = '=') "port=8080";;
- : int option = Some 4
# String.take_first 4 "port=8080";;
- : string = "port"
# String.replace_all ~sub:"-" ~by:"_" "max-retry-count";;
- : string = "max_retry_count"
# String.includes ~affix:"key" "api_key=secret";;
- : bool = true
# String.is_empty "";;
- : bool = true
# String.edit_distance "kitten" "sitting";;
- : int = 3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 string55_session.py
String.split_first ~sep:"=" "timeout=30" gave Some ("timeout", "30"), and None for a line with no separator, so
the missing case is a value the code must handle. split_all kept the empty field between two commas,
drop_first_while removed leading spaces, find_first_index found the = at index 4, and replace_all turned dashes
into underscores. String.edit_distance "kitten" "sitting" counted 3 edits, which is useful for “did you mean”
suggestions.
For real patterns, the distribution’s Str library works but keeps hidden state: its documentation says functions such
as Str.matched_string read what “the last call” to a matching function left behind. The re library, installed with
opam, avoids that and is the usual choice.
Checkpoint
Check: Bytes and characters in UTF-8 · The 5.4 and 5.5 String helpers
Practise: text, bytes and characters
The questions give strings, comparisons and lines of configuration and ask what OCaml 5.5.1 answers. Work each out, then compare with the sessions:
Practice that adapts to you
Practise this lesson
11 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): why can “abc” == “abc” be false while “abc” = “abc” is true?
= is structural equality: it compares two values by their contents, so two strings with the same characters, or
two lists with equal elements, are equal. == is physical equality: it is true only when both sides are the very same
value in memory. Two strings written separately are structurally equal but physically different, so "abc" = "abc"
is true while "abc" == "abc" is false. Use =, or a module’s equal function such as String.equal, to compare
data; == is for the rare code that needs to know whether two names refer to one mutable value. The same pair exists
for inequality: <> and !=.
Two harder questions follow, about a name that is cut in half and about building a large file of text. Plan your answer before you open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A job builds a large CSV export by appending each row to a string with ^ inside a loop, and it slows down sharply as the export grows. Explain why, and how you would rewrite it.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why can "abc" == "abc" be false while "abc" = "abc" is true?
The operator = compares contents (structural equality), while == is true only for the very same value in memory (physical equality). Two strings written separately are equal but not identical.
-
How should code compare what a user typed with "yes"?
With input = "yes" or String.equal input "yes". The operator == stays false for two different strings, even with the same characters.
-
What is an OCaml string, and when do Bytes and Buffer fit instead?
An immutable sequence of bytes, safe to share. Bytes changes bytes in place, and Buffer builds long text without copying it at every step.
-
Why is String.length of a six-character greeting in Devanagari 18?
It counts bytes, and each of those characters takes three bytes in UTF-8. Count and cut text by decoded characters with String.get_utf_8_uchar and the Uchar module.
-
When is the right operand of && or || evaluated?
Only when the left operand does not already decide the result: both operators short-circuit.
-
Which recent String functions handle everyday text without index arithmetic?
OCaml 5.5's String.split_first, split_all and replace_all, and 5.4's String.edit_distance.
Key takeaways
&&and||short-circuit,=compares contents while==compares identity, and chars and strings have their own literals and escapes.- Strings are immutable sequences of bytes, so they are safe to share, while Bytes changes bytes in place and Buffer builds long text without copying it at each step.
String.lengthcounts bytes, so UTF-8 text is counted and cut by decoded characters, withString.get_utf_8_ucharand the Uchar module.- OCaml 5.5’s String helpers such as
split_first,split_allandreplace_all, and 5.4’sedit_distance, handle everyday text without index arithmetic.
Terms in this lesson
- structural equality
- Comparing two values by their contents with =, so two separately built strings with the same characters are equal. == compares identity instead.
References
- The OCaml Manual: module String (Inria and the OCaml developers)
- The OCaml Manual: module Bytes (Inria and the OCaml developers)
- The OCaml Manual: module Buffer (Inria and the OCaml developers)
- The OCaml Manual: module Uchar (Inria and the OCaml developers)
- The OCaml Manual: the str library (Inria and the OCaml developers)
- ocaml-re: a regular expression library for OCaml (The ocaml-re developers)
Report a problem with this lesson
Kept only in this browser. Your Learn progress