Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 5 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answer to its interview question.

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:

Booleans, equality, chars and escapes Python · basics_session.py
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:

Bytes changed in place, a Buffer built piece by piece Python · bytes_buffer_session.py
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:

Interactive · answer and compare string, Bytes or Buffer for this text? Answer three questions about text your code handles; the types that do not fit are marked with the reason.
Will the text change after it has been built?
Is the text built from many pieces, for example in a loop?
Will other parts of the program keep and read the text?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to hold text against four things code may need from it
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

  1. Question 1 of 2 A login check reads if answer == "yes" then …, and it never succeeds, even when the user types yes. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: == asks whether both strings are the same value in memory, and the typed string is a different value

    Use answer = "yes" or String.equal answer "yes".

  2. Question 2 of 2 A report builds a 50,000-line string with s := !s ^ line ^ "\n" in a loop and gets slower and slower. What is the fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Add each line to a Buffer and take Buffer.contents at the end

    The Buffer documentation promises linear time, instead of the quadratic time of joining strings pair by pair.

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:

Bytes and characters in UTF-8 text Python · utf8_session.py
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:

The new String helpers on configuration lines Python · string55_session.py
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

  1. Question 1 of 2 How many bytes does String.length give for "héllo" in OCaml 5.5.1?

    Type a number.

    A hint

    é takes two bytes in UTF-8.

    Show the answer: Question 1 of 2

    Answer: 6 bytes

    h, the two bytes of é, l, l and o: 6, as the session printed.

  2. Question 2 of 2 What does String.split_first ~sep:"=" "host=example.org=x" give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Some ("host", "example.org=x")

    The option makes the missing-separator case explicit, as None did for "no separator here".

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.

  1. Question 1 of 11 In f x && g x, when is g x evaluated?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: Only when f x is true

    && and || short-circuit, so the right side can rely on what the left side checked.

  2. Question 2 of 11 What does Char.code 'a' give?

    Type a number.

    Show the answer: Question 2 of 11

    Answer: 97

    The byte value of a, as the session printed.

  3. Question 3 of 11 Why did the toplevel answer "tab:\there" for the string, but print_endline showed a real tab?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: The toplevel shows a string as a literal, with its escapes; printing writes the actual characters

    The toplevel writes values as OCaml would read them back; printing shows the bytes.

  4. Question 4 of 11 What happened when the session tried "abc".[0] <- 'x'?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: A syntax error saying strings are immutable, with a hint to use Bytes.set

    Immutability makes strings safe to share; Bytes is the mutable sibling.

  5. Question 5 of 11 let b = Bytes.of_string "hello" in Bytes.set b 0 'J'; Bytes.to_string b gives what?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: "Jello"

    The session printed - : string = "Jello".

  6. Question 6 of 11 The Devanagari word in the session has 6 characters of 3 bytes each. What did String.length give?

    Type a number.

    Show the answer: Question 6 of 11

    Answer: 18 bytes

    String.length counts bytes: 6 × 3 = 18.

  7. Question 7 of 11 A form truncates names with String.sub name 0 10 before saving them. What can go wrong for a name written in Tamil script?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: The cut can fall inside a character, leaving invalid UTF-8 at the end

    Decode with String.get_utf_8_uchar and cut at a character boundary; String.is_valid_utf_8 can check the result.

  8. Question 8 of 11 What does String.uppercase_ascii "héllo" give?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: "HéLLO"

    The session printed "HéLLO".

  9. Question 9 of 11 What does String.edit_distance "kitten" "sitting" give?

    Type a number.

    Show the answer: Question 9 of 11

    Answer: 3 edits

    k to s, e to i and an added g: 3, as the session printed.

  10. Question 10 of 11 What does String.split_all ~sep:"," "a,b,,c" give?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: ["a"; "b"; ""; "c"]

    The session printed ["a"; "b"; ""; "c"].

  11. Question 11 of 11 What does List.sort compare ["mango"; "Zebra"; "apple"] give?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: ["Zebra"; "apple"; "mango"]

    Capital letters have smaller byte values than small ones, so they sort first. Sort by String.lowercase_ascii when people will read the list.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. When is the right operand of && or || evaluated?

    Only when the left operand does not already decide the result: both operators short-circuit.

  6. 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.length counts bytes, so UTF-8 text is counted and cut by decoded characters, with String.get_utf_8_uchar and the Uchar module.
  • OCaml 5.5’s String helpers such as split_first, split_all and replace_all, and 5.4’s edit_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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.