Ocaml Basics for Beginners
Ocaml is a statically typed functional language.
Instead of using manual type annotations, it handles type inference using the Hindley-Milner algorithm.
The interpreter binary is ocaml & compiler is ocamlopt.
Setup
You can quickly try out small pieces of code or exercises by using the top-level, which is essentially a REPL (read -> evaluate -> print -> loop) for Ocaml.
You can follow this guide for setup and install.
Let Bindings
let is used to declare constants, variables, or functions.
You can chain let ... in construct like below:
ocaml
let x = 10 in
let y = 20 in
x+y
;
Let Ocaml bindings differ from declaring a variable in other imperative programming languages. Here, let is not mutable, i.e. the value of a let binding cannot be changed once it has been initialized. This is similar to the mathematical declaration of variables (y = f(x)), the value of y may be different for different values of x, but y cannot itself vary for the same value of x.
Functions
Below is a function to add two values, x & y. The final evaluated expression is returned as a result; there is no need to add any return keyword like Python, or Java.
Function Syntax
ocaml
(* let {function_name} *{variables} = {function_body} *)
let add x y = x + y
(* => Toplevel: int -> int -> int *)
If you declare this function in toplevel, it will print the function signature with the types of all input variables, followed by the function's return type.
In the above case, the type of x and y is int, and the last type represents the return type, which is also int.
The apostrophe as a suffix to var name " a' " represents the temporary declaration of variable a. This is similar to a_tmp in other languages.
The apostrophe as a prefix to a var name " 'a " means it's of arbitrary type.
Recursive function
ocaml
let rec fact n = if n=0 then 1 else n * fact (n-1);
(* Unit function *)
let print_hello = print_int "Hello, learning Ocaml!"
In order to make a function recursive, you need to add the rec keyword before the function name.
Parenthesis is not required while calling a function unless the argument is an expression.
A function that does not return any value is known as a unit function.
Partial functions
ocaml
let add x y = x+y;
=> val add : int -> int -> int = <fun>
(* Partial function. inc_5 returns a function. *)
let inc_5 = add 5;
=> val add_5 : int -> int = <fun>
inc_5 3;
=> :int = 8
Above is the definition of how you can declare partial functions. Also called function currying.
Anonymous functions
ocaml
(* Declare anonymous functions *)
let my_lambda = fun x -> x * x;
Operators
There are built-in operators in Ocaml. However, there is little distinction between them. Operators can be called functions and overridden.
Ocaml refrains from type conversion like int to float; instead, there are separate operators for floats. Add . after operator to deal with float values as mentioned below.
ocaml
(* Operators for floats *)
let divide_5 y = y /. 5.;
=> val divide_5 : float -> float = <fun>
let mean x y = (x +. y) /. 2.;
Data Structures
List
All values in a list should be of one type, separated by a semicolon. They are declared in square brackets. It's an immutable data structure.
ocaml
let lst = [8; 9; 8; 10];
let names = ["Mike"; "Sam"];
(* List Functions *)
List.nth names 1;
=> ""
List.map (fun x -> x* 2) last;
=> [16; 18; 16; 20]
(* Append element to the beginning of list *)
"shashank" :: names;
Append element to the beginning of list with "::" constructor called "cons" operator
Tuples
Values inside a tuple can have varying types. Optionally enclosed in parentheses, separated by commas.
ocaml
let tup = (1, 5, "Singapore", 15.5);
(* Parentheses is optional *)
let tup = 1, 5, "Singapore", 15.5;
Named Tuples / Records
Same as tuples, they have names of each variable. Also, called as records
ocaml
let record = {num: float; denom: int};
Arrays
They have values of one type enclosed within "[| |]". Arrays are mutable data types.
ocaml
let arr = [| 1;2;3; |];
arr.(0);
=> :int = 1
arr.(2);
=> :int = 3
An array can directly access any item with an index.
Strings & Characters
Strings should be enclosed in double quotes, while characters in single quotes.
Strings are not arrays of chars and hence should not be mixed together in an expression.
ocaml
let a = "shashank";
let a = 's';
Variants
Another powerful feature of Ocaml is variants. These are user-defined data types. It represents a value whose DS can be one of the several possibilities. It is similar to Enums in Python or Java with more features.
ocaml
(* Represents possible values of lang *)
type lang = Ocaml | Python | Java;
let x = Ocaml;
=> val x : lang = Ocaml
(* x is a type of lang *)
(* You can define much more complex constructors *)
type shape = Circle of float | Rectangle of int * int | Square of int;
let circle = Circle 5.;;
=> val circle : shape = Circle 5.
(* Ocaml will infer type of circle as shape *)
Each value in the variant is called a constructor.
Another important example of variants is the built-in 'a option type.
ocaml
type 'a option = Some of int | None;
'a will either have some value of type int or absence of value i.e. None.
The most common usage of variant types is to describe recursive data structures like tree
ocaml
type 'a btree = Empty | Node of 'a * 'a btree * 'a btree;
One crucial feature of Ocaml is there is no null data type, thus avoiding the Billion Dollar mistake. There will always be some value of absence of value represented by None.
Pattern Matching
One of the most important and awesome features of Ocaml is pattern matching. In simple terms, it's very similar to switch case constructs in other languages but much more powerful.
Can match an argument against:
- Exact Value
- Predicate
- Type constructor
ocaml
let is_zero y = match y with
| 0 -> true
| _ -> false
;
let is_even x = match x with
| x when x mod 2 = 0 -> true
| _ -> false
;
let calculate_area shape = match shape with
| Rectange (l,b) -> l * b
| Square x -> x * x
| Circle r -> int_of_float (3.14 *. r *. r)
;;
Exception Handling
Users can define exceptions with the "exception" keyword.
ocaml
exception EmptyList;
let find_first arr = match arr with
| [] -> raise EmptyList
| h :: _ -> h
;
find_first [];
=> Exception: EmptyList
Lazy Expressions
You can delay the evaluation of an expression when you actually need it. This is similar to generators in Python and sequences in Clojure. Lazy.force will evaluate the expression.
ocaml
let add a b = lazy (print_endline "Lazy evaluation"; a + b);
=> : int lazy_t = <lazy>
Lazy.force (add 2 3);
=> : int = 5
(* This will evaluate the expression *)
More details & solutions for a few exercises are here.