Beginner

Hello World in OCaml

Your first OCaml program - the classic Hello World example with Docker setup using the OCaml compiler

Every programming journey starts with Hello World. Let’s write our first OCaml program and explore what makes this functional language special.

The Code

Create a file named hello.ml:

1
print_endline "Hello, World!"

That’s it! Just one line for a complete OCaml program.

Understanding the Code

Let’s break down this simple program:

The print_endline Function

1
print_endline "Hello, World!"
  • print_endline - A built-in function that prints a string followed by a newline
  • "Hello, World!" - The string argument to print

Function Application in OCaml

OCaml uses space for function application, not parentheses:

1
2
3
4
5
(* OCaml style - function applied with space *)
print_endline "Hello, World!"

(* NOT like C-style languages *)
(* print_endline("Hello, World!")  -- Works but not idiomatic *)

The Entry Point

Unlike languages like Java or C that require a main function, OCaml executes expressions at the module’s top level. When you run the file, it evaluates each expression in order.

For explicit entry points, the convention is:

1
let () = print_endline "Hello, World!"

The let () = ... pattern makes it clear this is a side-effecting expression that returns unit (similar to void).

Running with Docker

The easiest way to run OCaml without installing anything locally is with Docker:

1
2
3
4
5
# Pull the official OCaml image
docker pull ocaml/opam:alpine

# Run the program
docker run --rm -v $(pwd):/app -w /app ocaml/opam:alpine ocaml hello.ml

Understanding the Docker Command

  • docker run --rm - Run a container and remove it when done
  • -v $(pwd):/app - Mount the current directory to /app in the container
  • -w /app - Set the working directory to /app
  • ocaml/opam:alpine - Use the official OCaml Docker image (Alpine-based)
  • ocaml hello.ml - Run the OCaml interpreter on our file

Compiling for Better Performance

The ocaml command interprets your program. For compiled, faster code:

1
2
3
4
5
# Bytecode compilation (portable)
docker run --rm -v $(pwd):/app -w /app ocaml/opam:alpine sh -c 'ocamlfind ocamlc hello.ml -o hello && ./hello'

# Native code compilation (fastest)
docker run --rm -v $(pwd):/app -w /app ocaml/opam:alpine sh -c 'ocamlfind ocamlopt hello.ml -o hello && ./hello'

Expected Output

Hello, World!

Alternative Approaches

OCaml provides several ways to output text:

Using print_string (no newline)

1
2
3
4
let () =
  print_string "Hello, ";
  print_string "World!";
  print_newline ()

Using Printf (formatted output)

1
2
let () =
  Printf.printf "Hello, %s!\n" "World"

The Printf module provides C-style format strings with type safety:

1
2
let () =
  Printf.printf "%s is %d years old\n" "OCaml" 28

Using Format (pretty printing)

1
2
let () =
  Format.printf "Hello, %s!@." "World"

The @. flushes the output buffer and adds a newline.

Multiple statements with semicolons

1
2
3
let () =
  print_string "Hello, ";
  print_endline "World!"

In OCaml, semicolons separate expressions (they don’t terminate statements like in C).

The Unit Type

In OCaml, every expression has a type. print_endline has the type:

1
val print_endline : string -> unit

This means:

  • It takes a string as input
  • It returns unit (written ())

The unit type is OCaml’s equivalent of void - it represents “no meaningful value.” It’s the return type of functions that exist only for their side effects.

1
2
3
4
(* This is how you explicitly write unit *)
let () = print_endline "Hello, World!"

(* The pattern () matches the unit value returned by print_endline *)

Type Inference

OCaml automatically infers types without explicit annotations:

1
2
3
4
5
(* The compiler knows this is string -> string *)
let greet name = "Hello, " ^ name ^ "!"

(* And this is string -> unit *)
let print_greeting name = print_endline (greet name)

You can check types in the REPL:

# print_endline;;
- : string -> unit = <fun>
# "Hello";;
- : string = "Hello"

Common Beginner Mistakes

Forgetting the Space

1
2
3
4
5
(* Wrong - this won't compile *)
let message = "Hello"^"World"

(* Right - operators need spaces around them *)
let message = "Hello" ^ " " ^ "World"

Wrong String Quotes

1
2
3
4
5
6
7
(* Double quotes for strings *)
print_endline "Hello, World!"

(* Single quotes are for characters (single char only) *)
let c = 'H'

(* NOT: print_endline 'Hello'  -- Wrong! Single quotes are for char *)

Missing Semicolons Between Expressions

1
2
3
4
5
6
7
8
9
(* Wrong - missing semicolon *)
let () =
  print_string "Hello"
  print_endline " World"  (* Error! *)

(* Right - use semicolon to sequence *)
let () =
  print_string "Hello";
  print_endline " World"

Mixing Up = and ==

1
2
3
4
5
(* = is for structural equality (use this!) *)
let same = (1 + 1 = 2)  (* true *)

(* == is for physical equality (reference comparison) *)
let physically_same = ("hi" == "hi")  (* may be false! *)

Installing OCaml Locally

If you prefer to run OCaml without Docker:

Using opam (Recommended):

1
2
3
4
5
6
7
8
9
# macOS
brew install opam
opam init
eval $(opam env)

# Ubuntu/Debian
apt install opam
opam init
eval $(opam env)

Direct installation:

1
2
3
4
5
# macOS with Homebrew
brew install ocaml

# Ubuntu/Debian
apt install ocaml

After installation:

1
2
3
4
5
6
# Run directly (interpreted)
ocaml hello.ml

# Or compile and run
ocamlopt hello.ml -o hello
./hello

The REPL: utop

OCaml has an interactive environment. The enhanced utop is recommended:

$ utop
────────────────────────────────────┬─────────────────────────────────────
                                    │ Welcome to utop version 2.13.1
────────────────────────────────────┴─────────────────────────────────────

utop # print_endline "Hello, World!";;
Hello, World!
- : unit = ()

utop # 1 + 2;;
- : int = 3

utop # let double x = x * 2;;
val double : int -> int = <fun>

utop # double 21;;
- : int = 42

Note: Lines must end with ;; in the REPL (but not in source files).

Useful REPL commands:

  • #quit;; or Ctrl+D - Exit
  • #use "file.ml";; - Load a file
  • #show module_name;; - Show module contents

A Bit of History

OCaml evolved from the ML family of languages:

  • 1972 - Robin Milner creates ML for the LCF proof assistant
  • 1987 - First Caml implementation at INRIA
  • 1990 - Caml Light with bytecode compiler
  • 1995 - Caml Special Light adds native compilation
  • 1996 - Objective Caml adds object system
  • 2011 - Renamed from “Objective Caml” to simply “OCaml”
  • 2022 - OCaml 5.0 brings multicore support

The name “Caml” originally stood for “Categorical Abstract Machine Language,” referring to its implementation technique.

Why OCaml for Hello World?

Even in this simple program, you see OCaml’s philosophy:

  1. Conciseness - One line does the job
  2. No boilerplate - No class definitions, main methods, or imports needed
  3. Type safety - The compiler ensures print_endline receives a string
  4. Functional style - Functions are the primary building blocks

Next Steps

Continue to Types and Functions to learn about OCaml’s powerful type system and how to define your own functions.

Key Takeaways

  1. print_endline prints a string with a newline
  2. Function application uses space, not parentheses
  3. let () = ... is the idiomatic entry point pattern
  4. unit (written ()) is OCaml’s void type
  5. Semicolons separate expressions, not terminate them
  6. Type inference means you rarely need type annotations
  7. ;; ends expressions in the REPL (not needed in files)

Running Today

All examples can be run using Docker:

docker pull ocaml/opam:alpine
Last updated: