Sunteți pe pagina 1din 16

10/14/2020 Lecture 4: Variant types and Polymorphism

Lecture 4: Variant Types and Polymorphism


Variant types
Lists are very useful, but it turns out they are not really as special as they look. We can
implement our own lists, and other more interesting data structures, such as binary trees.
In recitation you should have seen some simple examples of variant types sometimes
known as algebraic datatypes or just datatypes. Variant types provide some needed
power: the ability to have a variable that contains more than one kind of value.
Unlike tuple types and function types, but like record types, variant types cannot be
anonymous; they must be declared with their names. Suppose we wanted to have a variable
that could contain one of three values: Yes, No, or Maybe, very much like an enum in Java.
Its type could be declared as a variant type:

# type answer = Yes | No | Maybe;;


type answer = Yes | No | Maybe
# let x : answer = Yes;;
val x : answer = Yes

The type keyword declares a name for the new type.  The variant type is declared with a
set of constructors that describe the possible ways to make a value of that type. In this
case, we have three constructors: Yes, No, and Maybe. Constructor names must start with
an uppercase letter, and all other names in OCaml must start with a lowercase letter.
The different constructors can also carry other values with them. For example, suppose
we want a type that can either be a 2D point or a 3D point. It can be declared as follows:

type eitherPoint = TwoD of float * float


| ThreeD of float * float * float

Some examples of values of type eitherPoint are: TwoD (2.1, 3.0) and ThreeD
(1.0, 0.0, -1.0).
Suppose we have a value of type eitherPoint, which is either a TwoD of something or
ThreeD of something. We need a way to extract the "something". This can be done with
pattern matching. Example:

let lastTwoComponents (p : eitherPoint) : float * float =


match p with
TwoD (x, y) -> (x, y)
| ThreeD (x, y, z) -> (y, z)

Variant type syntax


We use X as a metavariable to represent the name of a constructor, and T to represent
the name of a type. Optional syntactic elements are indicated by brackets [ ]. Then a variant
type declaration looks like this in general:

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 1/16
10/14/2020 Lecture 4: Variant types and Polymorphism

type T = X1 [of t1] | ... | Xn [of tn]

Variant types introduce new syntax for terms e, patterns p, and values v:

e ::= ... |  X e |  match e with p1 -> e1 | ... | pn -> en


p ::=  X |  X(x1 : t1, ...,  xn : tn)
v ::= c | (v1, ..., vn) | fun p -> e | X v

Note that the vertical bars in the expression "match e with p1 -> e1 | ... | pn -> en"
are part the syntax of this construct; the other vertical bars (|) are part of the BNF notation.
We can use variant types to define many useful data structures.  In fact, the bool is really
just a variant type with constructors named true and false.

Implementing integer lists


We can write our own version of lists using variant types. Suppose we want to define values
that act like linked lists of integers. A linked list is either empty, or it has an integer followed by
another list containing the rest of the list elements. This leads to a very natural variant type
declaration:

type intlist = Nil | Cons of (int * intlist)

This type has two constructors, Nil and Cons. It is a recursive type because it mentions
itself in its own definition (in the Cons constructor), just like a recursive function is one that
mentions itself in its own definition.
Any list of integers can be represented by using this type. For example, the empty list is
just the constructor Nil, and Cons corresponds to the operator ::. Here are some
examples of lists:

let list1 = Nil (* the empty list: [] *)


let list2 = Cons (1, Nil) (* the list containing just 1: [1] *)
let list3 = Cons (2, Cons (1, Nil)) (* the list [2; 1] *)
let list4 = Cons (2, list2) (* also the list [2; 1] *)
(* the list [1; 2; 3; 4; 5] *)
let list5 = Cons (1, Cons (2, Cons (3, Cons (4, Cons (5, Nil)))))
(* the list [6; 7; 8; 9; 10] *)
let list6 = Cons (6, Cons (7, Cons (8, Cons (9, Cons (10, Nil)))))

So we can construct any lists we want. We can also take them apart using pattern
matching. For example, our length function above can be written for intlists by just
translating the list patterns into the corresponding patterns using constructors.  Similarly, we
can implement many other functions over lists, as shown in the following examples.

(* An intlist is either Nil or Cons of an int and a (shorter) intlist *)


type intlist = Nil | Cons of int * intlist

(* Returns the length of lst *)


let rec length (lst : intlist) : int =

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 2/16
10/14/2020 Lecture 4: Variant types and Polymorphism
match lst with
| Nil -> 0
| Cons (h, t) -> length t + 1

(* is the list empty? *)


let is_empty (lst : intlist) : bool =
match lst with
| Nil -> true
| Cons _ -> false

(* Notice that the match expressions for lists all have the same
* form -- a case for the empty list (Nil) and a case for a Cons.
* Also notice that for most functions, the Cons case involves a
* recursive function call. *)

(* Return the sum of the elements in the list *)


let rec sum (lst : intlist) : int =
match lst with
| Nil -> 0
| Cons (i, t) -> i + sum t

(* Create a string representation of a list *)


let rec to_string (lst : intlist) : string =
match lst with
| Nil -> ""
| Cons (i, Nil) -> string_of_int i
| Cons (i, Cons (j, t)) ->
string_of_int i ^ "," ^ to_string (Cons (j, t))

(* Return the head (first element) of the list *)


let head (lst : intlist) : int =
match lst with
| Nil -> failwith "empty list"
| Cons (i, t) -> i

(* Return the tail (rest of the list after the head) *)


let tail (lst : intlist) : intlist =
match lst with
| Nil -> failwith "empty list"
| Cons (i, t) -> t

(* Return the last element of the list (if any) *)


let rec last (lst : intlist) : int =
match lst with
| Nil -> failwith "empty list"
| Cons (i, Nil) -> i
| Cons (i, t) -> last t

(* Return the nth element of the list (starting from 0) *)


https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 3/16
10/14/2020 Lecture 4: Variant types and Polymorphism
let rec nth (lst : intlist) (n : int) : int =
match lst with
| Nil -> failwith "index out of bounds"
| Cons (i, t) ->
if n = 0 then i
else nth t (n - 1)

(* Append two lists: append [1; 2; 3] [4; 5; 6] = [1; 2; 3; 4; 5; 6] *)


let rec append (l1 : intlist) (l2 : intlist) : intlist =
match l1 with
| Nil -> l2
| Cons (i, t) -> Cons (i, append t l2)

(* Reverse a list: reverse [1; 2; 3] = [3; 2; 1].


* First reverse the tail of the list
* (e.g., compute reverse [2; 3] = [3; 2]), then
* append the singleton list [1] to the end to yield [3; 2; 1].
* This is not the most efficient method. *)
let rec reverse (lst : intlist) : intlist =
match lst with
| Nil -> Nil
| Cons (h, t) -> append (reverse t) (Cons (h , Nil))

(******************************
* Examples
******************************)

(* Here is a way to perform a function on each element


* of a list. We apply the function recursively.
*)

let inc (x : int) : int = x + 1


let square (x : int) : int = x * x

(* Given [i1; i2; ...; in], return [i1+1; i2+1; ...; in+n] *)
let rec addone_to_all (lst : intlist) : intlist =
match lst with
| Nil -> Nil
| Cons (h, t) -> Cons (inc h, addone_to_all t)

(* Given [i1; i2; ...; in], return [i1*i1; i2*i2; ...; in*in] *)
let rec square_all (lst : intlist) : intlist =
match lst with
| Nil -> Nil
| Cons (h, t) -> Cons (square h, square_all t)

(* Here is a more general method. *)

(* Given a function f and [i1; ...; in], return [f i1; ...; f in].
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 4/16
10/14/2020 Lecture 4: Variant types and Polymorphism
* Notice how we factored out the common parts of addone_to_all
* and square_all. *)
let rec do_function_to_all (f : int -> int) (lst : intlist) : intlist =
match lst with
| Nil -> Nil
| Cons (h, t) -> Cons (f h, do_function_to_all f t)

let addone_to_all (lst : intlist) : intlist =


do_function_to_all inc lst

let square_all (lst : intlist) : intlist =


do_function_to_all square lst

(* Even better: use anonymous functions. *)

let addone_to_all (lst : intlist) : intlist =


do_function_to_all (fun x -> x + 1) lst

let square_all (lst : intlist) : intlist =


do_function_to_all (fun x -> x * x) lst

(* Equivalently, we can partially evaluate by applying


* do_function_to_all just to the first argument. *)

let addone_to_all : intlist -> intlist =


do_function_to_all (fun x -> x + 1)

let square_all : intlist -> intlist =


do_function_to_all (fun x -> x * x)

(* Say we want to compute the sum and product of integers


* in a list. *)

(* Explicit versions *)
let rec sum (lst : intlist) : int =
match lst with
| Nil -> 0
| Cons (i, t) -> i + sum t

let rec product (lst : intlist) : int =


match lst with
| Nil -> 1
| Cons (h, t) -> h * product t

(* Better: use a general function collapse that takes an


* operation and an identity element for that operation.
*)

(* Given f, b, and [i1; i2; ...; in], return f(i1, f(i2, ..., f (in, b))).
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 5/16
10/14/2020 Lecture 4: Variant types and Polymorphism
* Again, we factored out the common parts of sum and product. *)
let rec collapse (f : int -> int -> int) (b : int) (lst : intlist) : int =
match lst with
| Nil -> b
| Cons (h, t) -> f h (collapse f b t)

(* Now we can define sum and product in terms of collapse *)


let sum (lst : intlist) : int =
let add (i1 : int) (i2 : int) : int = i1 + i2 in
collapse add 0 lst

let product (lst : intlist) : int =


let mul (i1 : int) (i2 : int) : int = i1 * i2 in
collapse mul 1 lst

(* Here, we use anonymous functions instead of defining add and mul.


* After all, what's the point of giving those functions names if all
* we're going to do is pass them to collapse? *)
let sum (lst : intlist) : int =
collapse (fun i1 i2 -> i1 + i2) 0 lst

let product (lst : intlist) : int =


collapse (fun i1 i2 -> i1 * i2) 1 lst

(* Trees of integers *)

type inttree = Empty | Node of node


and node = { value : int; left : inttree; right : inttree }

(* Return true if the tree contains x. *)


let rec search (t : inttree) (x : int) : bool =
match t with
| Empty -> false
| Node {value=v; left=l; right=r} ->
v = x || search l x || search r x

let tree1 =
Node {value=2; left=Node {value=1; left=Empty; right=Empty};
right=Node {value=3; left=Empty; right=Empty}}

let z = search tree1 3

Representing trees with a recursive type

Trees are another very useful data structure. Unlike lists, they are not built into OCaml. A
binary tree is either

the empty tree (no children), or


a node containing a value and two children that are binary trees.
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 6/16
10/14/2020 Lecture 4: Variant types and Polymorphism

Just for variety, let's use a record type to represent a tree node. In OCaml we have to define
two mutually recursive types, one to represent a tree node, and one to represent a (possibly
empty) tree:

type inttree = Empty | Node of node


and node = { value : int; left : inttree; right : inttree }

The rule for when mutually recursive type declarations are legal is a little tricky. Essentially,
any cycle of recursive types must include at least one record or variant type. Since the cycle
between inttree and node includes both kinds of types, this declaration is legal.

2
/ \ Node {value=2; left=Node {value=1; left=Empty; right=Empty};
1 3 right=Node {value=3; left=Empty; right=Empty}}

Because there are several things stored in a tree node, it's helpful to use a record rather
than a tuple to keep them all straight. But a tuple would also have worked.
We can use pattern matching to write the usual algorithms for recursively traversing trees.
For example, here is a recursive search over the tree:

(* Return true if the tree contains x. *)


let rec search ((t: inttree), (x:int)): bool =
match t with
Empty -> false
| Node {value=v; left=l; right=r} ->
v = x || search (l, x) || search (r, x)

Of course, if we knew the tree obeyed the binary search tree invariant, we could have
written a more efficient algorithm.

Representing natural numbers with a recursive type

We can even define data structures that act like numbers, demonstrating that we don't
really have to have numbers built into OCaml either! A natural number is either the value
zero or the successor of some other natural number. This definition leads naturally to the
following definition for values that act like natural numbers nat:

type nat = Zero | Next of nat

This is how you might define the natural numbers in a mathematical logic course. We have
defined a new type nat, and Zero and Next are constructors for values of this type. The
type nat is a recursive type, which allows us to build expressions that have an arbitrary
number of nested Next constructors. Such values act like natural numbers:

let zero = Zero


and one = Next Zero
and two = Next (Next Zero)
let three = Next two
let four = Next three

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 7/16
10/14/2020 Lecture 4: Variant types and Polymorphism

When we ask the interpreter what four represents, we get

four;;
- : nat = Next (Next (Next (Next Zero)))

The equivalent Java definitions would be

public interface nat { }


public class Zero implements nat {}
public class Next implements nat {
nat v;
Next(nat v) { v = this.v; }
}

nat zero = new Zero();


nat one = new Next(new Zero());
nat two = new Next(new Next(new Zero()));
nat three = new Next(two);
nat four = new Next(three);

And in fact the implementation is similar.


Now we can write functions to manipulate values of this type.

let isZero (n : nat) : bool =


match n with
Zero -> true
| Next m -> false

Here we're pattern-matching a value with type nat. If the value is Zero we evaluate to
true; otherwise we evaluate to false.

let pred (n : nat) : nat =


match n with
Zero -> failwith "Zero has no predecessor"
| Next m -> m

Here we determine the predecessor of a number. If the value of n matches Zero then we
raise an exception, since zero has no predecessor in the natural numbers. If the value
matches Next m for some value m (which of course also must be of type nat), then we
return m.
Similarly we can define a function to add two numbers:

let rec add (n1 : nat) (n2 : nat) : nat =


match n1 with
Zero -> n2
| Next m -> add m (Next n2)

If you were to try evaluating add four four, the interpreter would respond with:

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 8/16
10/14/2020 Lecture 4: Variant types and Polymorphism

add four four;;


- : nat = Next (Next (Next (Next (Next (Next (Next (Next Zero)))))))

which is the nat representation of 8.


To better understand the results of our computation, we would like to convert such values
to type int:

let rec toInt (n : nat) : int =


match n with
Zero -> 0
| Next n -> 1 + toInt n

That was pretty easy. Now we can write toInt (add four four) and get 8. How
about the inverse operation?

let rec toNat (i : int) : nat =


if i < 0 then failwith "toNat on negative number"
else if i = 0 then Zero
else Next (toNat (i - 1))

To determine whether a natural number is even or odd, we can write a pair of mutually
recursive functions:

let rec even (n : nat) : bool =


match n with
Zero -> true
| Next n -> odd n
and odd (n : nat) : bool =
match n with
Zero -> false
| Next n -> even n

You have to use the keyword and to combine mutually recursive functions like this.
Otherwise the compiler would give an error when you refer to odd before it has been
defined.
Finally we can define multiplication in terms of addition.

let rec mul (n1 : nat) (n2 : nat) : nat =


match n1 with
Zero -> Zero
| Next m -> add n2 (mul m n2)

which gives

toInt (mul (toNat 5) (toNat 20));;


- : int = 100

Pattern matching
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 9/16
10/14/2020 Lecture 4: Variant types and Polymorphism

It turns out that the syntax of OCaml patterns is richer than what we saw in the last lecture.
In addition to new kinds of terms for creating and projecting tuple and record values, and
creating and examining variant type values, we also have the ability to match patterns
against values to pull them apart into their parts.
When used properly, pattern matching leads to concise, clear code.  This is because 
OCaml pattern matching allows one pattern to appear as a subexpression of another pattern.
For example, we see above that Next n is a pattern, but so is Next (Next n). This
second pattern matches only on a value that has the form Next (Next v) for some value v
(that is, the successor of the successor of something), and binds the variable n to that
something, v.
Similarly, in our implementation of the nth function, earlier, a neat trick is to use pattern
matching to do the if n = 0 and the match at the same time. We pattern-match on the
tuple (lst, n):

(* Returns the nth element of lst *)


let rec nth lst n =
match (lst, n) with
(h :: t, 0) -> h
| (h :: t, _) -> nth (t, n - 1)
| ([], _) -> failwith "nth applied to empty list"

Here, we've also added a clause to catch the empty list and raise an exception. We're also
using the wildcard pattern _ to match on the n component of the tuple, because we don't
need to bind the value of n to another variable—we already have n. We can make this code
even shorter; can you see how?

Example: Pattern matching on records

All natural numbers are nonnegative, but we can simulate integers in terms of the naturals
by using a representation consisting of a sign and magnitude:

type sign = Pos | Neg


type integer = { sign : sign; mag : nat }

Here we've defined integer to refer to a record type with two fields: sign and mag.
Remember that records are unordered, so there is no concept of a "first" field.
The declarations of sign and integer both create new types.  However, it is possible to
write type declarations that simply introduce a new name for an existing type. For example, if
we wrote type number = int, then the types number and int could be used
interchangeably.
We can use the definition of integer to write some integers:

let zero = {sign=Pos; mag=Zero}


let zero' = {sign=Neg; mag=Zero}
let one = {sign=Pos; mag=Next Zero}
let negOne = {sign=Neg; mag=Next Zero}

Now we can write a function to determine the successor of any integer:


https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 10/16
10/14/2020 Lecture 4: Variant types and Polymorphism

let inc (i : integer) : integer =


match i with
{sign = _; mag = Zero} -> {sign = Pos; mag = Next Zero}
| {sign = Pos; mag = n} -> {sign = Pos; mag = Next n}
| {sign = Neg; mag = Next n} -> {sign = Neg; mag = n}

Here we're pattern-matching on a record type. Notice that in the third pattern we are doing
pattern matching because the mag field is matched against a pattern itself, Next n.
Remember that the patterns are tested in order. How does the meaning of this function
change if the first two patterns are swapped?
The predecessor function is very similar, and it should be obvious that we could write
functions to add, subtract, and multiply integers in this representation.

OCaml syntax

Taking into account the ability to write complex patterns, we can now write down a more
comprehensive syntax for OCaml.
syntactic syntactic variables and grammar
examples
class rule(s)
identifiers x, y a, x, y, x_y, foo1000, ...
datatypes,
datatype X, Y Nil, Cons, list
constructors
...~2, ~1, 0, 1, 2 (integers)
1.0, ~0.001, 3.141 (floats)
constants c true, false (booleans)
"hello", "", "!" (strings)
#"A", #" " (characters)
unary
operator
u ~, not, size, ...
binary
operators
b +, *, -, >, <, >=, <=, ^, ...

e ::- c | x | u e | e1 b e2
| if e1 then e2 else e3 |
expressions let d1...dn in e end | e (e1, ..., en) | ~0.001, foo, not b,
(terms) (e1,...,en) | #n e | 2 + 2, Cons(2, Nil)
{x1=e1, ..., xn=en} | #x e | X(e) |
match e with p1->e1 | ... | pn->en

p ::= c | x | (p1,..., pn) | {x1= a:int, (x:int,y:int),


patterns
p1,...,xn= pn} | X | X ( p ) I(x:int)

d ::= val p = e | fun y p : t - e | val one = 1


declarations datatype Y - X1 [of t1] | ... | Xn [of fun square(x: int): int
tn] datatype d - N | I of int

t ::= int | float | bool | string


| char | t1->t2 | t1*...*tn | int, string, int->int,
types bool*int->bool
{x1:t1, x2:t2,..., xn:tn} | Y
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 11/16
10/14/2020 Lecture 4: Variant types and Polymorphism
values v ::= c | (v1,...,vn) | 2, (2,"hello"), Cons(2,Nil)
{x1=v1, ..., xn=vn} | X(v)

Note: pattern-matching floating point constants is not possible. So in the production "p ::=
c | .." above, c is an integer, boolean, string, or character constant, but not float.

Polymorphism
There is a nice feature that allows us to avoid rewriting the same code over and over so
that it works for different types. Suppose we want to write a function that swaps the position
of values in an ordered pair:

let swapInt ((x : int), (y : int)) : int * int = (y, x)


and swapReal ((x : float), (y : float)) : float * float = (y, x)
and swapString ((x : string), (y : string)) : string * string = (y, x)

This is tedious, because we're writing exactly the same algorithm each time. It gets worse!
What if the two pair elements have different types?

let swapIntReal ((x : int), (y : float)) : float * int = (y, x)


and swapRealInt ((x : float), (y : int)) : int * float = (y, x)

And so on. There is a better way:

# let swap ((x : 'a), (y : 'b)) : 'b * 'a = (y, x);;


val swap : 'a * 'b -> 'b * 'a = <fun>

Instead of writing explicit types for x and y, we write type variables 'a and 'b.  The type of
swap is 'a * 'b -> 'b * 'a. This means that we can use swap as if it had any type that
we could get by consistently replacing 'a and 'b in its type with a type for 'a and a type for
'b. We can use the new swap in place of all the old definitions:

swap (1, 2) (* (int * int) -> (int * int) *)


swap (3.14, 2.17) (* (float * float) -> (float * float) *)
swap ("foo", "bar") (* (string * string) -> (string * string) *)
swap ("foo", 3.14) (* (string * float) -> (float * string) *)

In fact, we can leave out the type declarations in the definition of swap, and OCaml will
figure out the most general polymorphic type it can be given, automatically:

# let swap (x, y) = (y, x);;


val swap : 'a * 'b -> 'b * 'a = <fun>

The ability to use swap as though it had many different types is known as polymorphism,
from the Greek for "many forms".
Notice that the type variables must be substituted consistently in any use of a polymorphic
expression. For example, it is impossible for swap to have the type (int * float) ->
(string * int), because that type would consistently substitute for the type variable 'a
but not for 'b.

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 12/16
10/14/2020 Lecture 4: Variant types and Polymorphism

OCaml programmers typically read the types 'a and 'b as "alpha" and "beta". This is
easier than saying "single quotation mark a" or "apostrophe a". They also they wish they
could write Greek letters instead. A type variable may be any identifier preceded by a single
quotation mark; for example, 'key and 'value are also legal type variables. The OCaml
compiler needs to have these identifiers preceded by a single quotation mark so that it
knows it is seeing a type variable.
It is important to note that to be polymorphic in a parameter x, a function may not use x in
any way that would identify its type. It must treat x as a black box. Note that swap doesn't
use its arguments x or y in any interesting way, but treats them as black boxes. When the
OCaml type checker is checking the definition of swap, all it knows is that x is of some
arbitrary type 'a. It doesn't allow any operation to be performed on x that couldn't be
performed on an arbitrary type. This means that the code is guaranteed to work for any x
and y. However, we can apply other polymorphic functions. For example,

# let appendToString ((x : 'a), (s : string), (convert : 'a -> string)) : string =
(convert x) ^ " " ^ s;;
val appendToString : 'a * string * ('a -> string) -> string = <fun>
# appendToString (3110, "class", string_of_int);;
- : string = "3110 class"
# appendToString ("ten", "twelve", fun (s : string) -> s ^ " past");;
- : string = "ten past twelve"

Parameterized Types
We can also define polymorphic datatypes. For example, we defined lists of integers as

type intList = Nil | Cons of (int * intList)

But we can make this more general by using a parameterized variant type instead:

type 'a list_ = Nil | Cons of ('a * 'a list_)

A parameterized datatype is a recipe for creating a family of related datatypes. The name


'a is a type parameter for which any other type may be supplied. For example, int list_
is a list of integers, float list_ is a list of float, and so on. However, list_ itself is not a
type. Notice also that we cannot use list_ to create a list each of whose elements can be
any type. All of the elements of a T list_ must be T's.

let il : int list_ = Cons (1, Cons (2, Cons (3, Nil))) (* [1; 2; 3] *)
let fl : float list_ = Cons (3.14, Cons (2.17, Nil)) (* [3.14; 2.17] *)
let sl : string list_ = Cons ("foo", Cons ("bar", Nil)) (* ["foo"; "bar"] *)
let sil : (string * int) list_ =
Cons (("foo", 1), Cons (("bar", 2), Nil)) (* [("foo", 1); ("bar", 2)] *)

Notice list_ itself is not a type. We can think of list_ as a function that, when applied
to a type like int, produces another type (int list_). It is a parameterized type
constructor: a function that takes in parameters and gives back a type. Other languages

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 13/16
10/14/2020 Lecture 4: Variant types and Polymorphism

have parameterized type constructors. For example, in Java you can declare a
parameterized class:

class List<T> {
T head;
List <T> tail;
...
}

In OCaml, we can define polymorphic functions that know how to manipulate any kind of
list:

(* polymorphic lists *)

type 'a list_ = Nil | Cons of 'a * 'a list_

(* is the list empty? *)


let is_empty (lst : 'a list_) : bool =
match lst with
| Nil-> true
| _ -> false

(* length of the list *)


let rec length (lst : 'a list_) : int =
match lst with
| Nil-> 0
| Cons (_, rest) -> 1 + length rest

(* append [a; b; c] [d; e; f] = [a; b; c; d; e; f] *)


let rec append (x : 'a list_) (y : 'a list_) : 'a list_ =
match x with
| Nil-> y
| Cons (h, t) -> Cons (h, append t y)

(* [1; 2; 3] *)
let il = Cons (1, Cons (2, Cons (3, Nil)))
let il2 = append il il
let il4 = append il2 il2
let il8 = append il4 il4
(* ["a"; "b"; "c"] *)
let sl = Cons ("a", Cons ("b", Cons ("c", Nil)))
let sl2 = append sl sl
let sl4 = append sl2 sl2

(* reverse the list: reverse [1; 2; 3; 4] = [4; 3; 2; 1] *)


let rec reverse (x : 'a list_) : 'a list_ =
match x with
| Nil-> Nil
| Cons (h, t) -> append (reverse t) (Cons (h, Nil))

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 14/16
10/14/2020 Lecture 4: Variant types and Polymorphism

let il4r = reverse il4


let sl4r = reverse sl4

(* apply the function f to each element of x


* map f [a; b; c] = [f a; f b; f c] *)
let rec map (f : 'a -> 'b) (x : 'a list_) : 'b list_ =
match x with
| Nil-> Nil
| Cons (h, t) -> Cons (f h, map f t)

let mil4 = map string_of_int il4

(* insert sep between each element of x:


* separate s [a; b; c; d] = [a; s; b; s; c; s; d] *)
let rec separate (sep : 'a) (x : 'a list_) : 'a list_ =
match x with
| Nil-> Nil
| Cons (h, Nil) -> x
| Cons (h, t) -> Cons (h, Cons (sep, separate sep t))

let s0il4 = separate 0 il4

For trees,

type 'a tree = Leaf | Node of ('a tree) * 'a * ('a tree)

If we use a record type for the nodes, the record type also must be parameterized, and
instantiated on the same element type as the tree type:

type 'a tree = Leaf | Node of 'a node


and 'a node = {left: 'a tree; value: 'a; right: 'a tree}

It is also possible to have multiple type parameters on a parameterized type, in which case
parentheses are needed:

type ('a, 'b) pair = {first: 'a; second: 'b};;


let x = {first=2; second="hello"};;
val x: (int, string) pair = {first = 2; second = "hello"}

Abstract syntax and variant types


Earlier we noticed that there is a similarity between BNF declarations and variant type
declarations. In fact, we can define variant types that act like the corresponding BNF
declarations. The values of these variant types then represent legal expressions that can
occur in the language. For example, consider a BNF definition of legal OCaml type
expressions:
(base types) b ::= int | float | string | bool | char
https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 15/16
10/14/2020 Lecture 4: Variant types and Polymorphism
(types) t ::= b | t -> t | t1 * t2 *...* tn | { x1 : t1; ...; xn : tn } | X

This grammar has exactly the same structure as the following type declarations:

type id = string
type baseType = Int | Real | String | Bool | Char
type mlType = Base of baseType | Arrow of mlType * mlType
| Product of mlType list | Record of (id * mlType) list
| DatatypeName of id

Any legal OCaml type expression can be represented by a value of type mlType that
contains all the information of the corresponding type expression. This value is known as the
abstract syntax for that expression. It is abstract because it doesn't contain any information
about the actual symbols used to represent the expression in the program. For example, the
abstract syntax for the expression int * bool -> {name : string} would be:

Arrow (Product (Cons (Base Int, Cons (Base Bool, Nil))),


Record (Cons (("name", Base String), Nil)))

The abstract syntax would be exactly the same even for a more verbose version of the
same type expression: ((int * bool) -> {name : string}). Compilers typically use
abstract syntax internally to represent the program that they are compiling. We will see a lot
more abstract syntax later in the course when we see how OCaml works.

https://www.cs.cornell.edu/courses/cs3110/2014sp/lectures/4/variant-types-and-polymorphism.html 16/16

S-ar putea să vă placă și