After expressions and variables, we finally dive into functions in OCaml today. The whole point of functional programming is to define functions and to apply them. Functions are constructed by the keyword
fun, which takes an argument and an expression as the body of the function, e.g.
# fun n -> n + 1;; - : int -> int = <fun>
A function’s type is determined by the types of its argument and return value, written as
int -> int in this example. The first
int is the type of argument while the second one is of the return value. The above expression, called lambda expression, creates an anonymous function. This makes sense because the name is not an essential part of a function. Mathematically, a function is a mapping between a set of inputs and a set of permissible outputs and each input is mapped to exactly one output. It is not important at all whatever this mapping is called. We can directly apply this anonymous function on a valid input. For example,
# (fun n -> n + 1) 1;; - : int = 2
However, it is not convenient if we want to apply this function many times on different inputs. Since a function is just a value, we can bind a name to it with
let construct and apply the function with the name any times later.
# let inc = fun n -> n + 1;; val inc : int -> int = <fun> # inc 1;; - : int = 2 # inc 2;; - : int = 3
Because function definitions are almost everywhere in programs, there is a syntactic sugar to make it less verbose:
# let inc n = n + 1;; val inc : int -> int = <fun>
It is possible to define functions taking multiple arguments.
# let sum x y = x + y;; val sum : int -> int -> int = <fun>
However, it is mere syntactic sugar because lambda calculus (and thus functional languages) has only unary functions theoretically. The type of above function is
int -> int -> int, which should be read as
int -> (int -> int), i.e. a function taking
int and returning another function
int -> int. That is, OCaml translates the evaluation of function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of unary functions. This technique is called currying in honor of Haskell Curry, who had a significant impact on the design and theory of programming languages. Therefore, it is natural that we can partially apply a multi-argument function. For example, we can redefine the function
inc by partially applying the function
# let inc = sum 1;; val inc : int -> int = <fun> # inc 2;; - : int = 3
In this way, multi-argument functions can be regarded as nested functions. Therefore, we have another way to define multi-argument functions.
# let f i = let g j = i + j in g;; val f : int -> int -> int = <fun>
g is a function nested inside
f, which is not allowed in C/C++/Java. It is important to note that when we use a variable defined earlier in a function, the binding is static in lexical order (again) as illustrated by the following example.
# let i = 1;; val i : int = 1 # let add j = i + j;; val add : int -> int = <fun> # let i = 2;; val i : int = 2 # let x = add 1;; val x : int = 2
Although we bind
i to a new value after defining the function
add, it still uses the old binding. It is another evidence that “variables” don’t vary in OCaml. The second
i just reuses the name rather than assigning it a new value.
As said many times, functional language supports passing functions as arguments to other functions and returning them as the values from other functions. A function that does one of these (or both) is called higher-order function. The derivative in calculus is a common example:
# let d dx f = (fun x -> (f (x +. dx) -. f x) /. dx);; val d : float -> (float -> float) -> float -> float = <fun> # let df = d 1e-10;; val df : (float -> float) -> float -> float = <fun> # let sqr x = x *. x;; val sqr : float -> float = <fun> # let f = df sqr;; val f : float -> float = <fun> # let y = f 2.;; val y : float = 4.000000330961484
An interesting and natural fact is that operators are functions too in OCaml. Infix operators really only differ syntactically from other functions. In fact, an infix operator can also be used as a prefix operator by surrounding the operator with parens and putting it in a prefix position. The only caution is to keep some space between * and parens if * is a part of operator name. Otherwise, the parser will treat it as comments. As functions, binary operators can be partially applied.
# let inc = (+) 1;; val inc : int -> int = <fun> # inc 2;; - : int = 3
Since operators are functions, we of course can redefine them, i.e. binding the name of a predefined operator to a new function.
# let (+) x y = x - y;; val ( + ) : int -> int -> int = <fun> # 1 + 2;; - : int = -1
This is a silly and dangerous usage. In general, one should not abuse this feature. Besides, this is not overloading. In fact, functions (including operators) can’t have overloaded definitions in OCaml to make type inference easier. On the other hand, we can define new operators in OCaml. A function is treated syntactically as an infix operator if the name of function starts with the infix symbols
= @ ^ ∣ & + - * / $ %
followed by zero or more operator characters
! $ % & * + - . / : ? @ ^ ∣ ~
The precedence and associativity of new infix operators is determined by its first character in the operator name. User-defined operators make it easier to implement nice-looking embedded languages. For example,
# let (|>) x f = f x ;; val ( |> ) : 'a -> ('a -> 'b) -> 'b = <fun>
It is a pipe operator, similar to the one in the UNIX shell. Here is an application:
# 1. |> (+.) 2. |> sqr;; - : float = 9.
In the future posts, we will see a lot of recursive functions. In functional programming, recursion is used a lot more often than iterations. However, the
let construct is not sufficient to define recursive functions because the bounded name is only available after the
let expression. Therefore, OCaml provides the keyword
let rec to define recursive functions.
# let rec pow x i = if i = 0 then 1 else x * pow x (i-1);; val pow : int -> int -> int = <fun>
Mutually recursive functions are also possible with the keyword
# let rec is_even n = if n == 0 then true else is_odd (n - 1) and is_odd n = if n == 0 then false else is_even (n - 1);; val is_even : int -> bool = <fun> val is_odd : int -> bool = <fun>
Here is a sloppy example for demo purpose only. No one will really implement is_even or is_odd in this way.
Finally, let’s talk about the labeled and optional arguments. Like Python, OCaml supports labeled arguments, which let us identify a function argument by name. Labeled arguments are very attractive for functions with lots of arguments because they are self-documented and can be passed in a different order than the one of their definition. Labeled arguments are marked by a leading tilde in the declaration of functions. In the function applications, the label (and leading tilde) is in front of the variable to be passed separated by a colon.
# let sum ~x ~y = x + y;; val sum : x:int -> y:int -> int = <fun> # let x = 1;; val x : int = 1 # let z = 2;; val z : int = 2 # sum ~y:z ~x;; - : int = 3
The above example also shows the usage of label punning, i.e. dropping the text after the colon if the label and the name of variable being used are the same.
Arguments with default values are called optional arguments, and can be omitted in function calls — the corresponding default values will be used. Optional arguments are passed in the same syntax as labeled arguments and can be in any order just like labeled arguments.
# let sum ?(x=0) y = x + y;; val sum : ?x:int -> int -> int = <fun> # sum 2;; - : int = 2 # sum ~x:1 2;; - : int = 3
We have touched several aspects of functions in OCaml. Again there are plenty of details left and you are encouraged very much to explore more in the official OCaml website. In the next post, I will talk about the powerful control structure: pattern matching.