• Logistics:
    • See Piazza for link to join class today
    • Next assignment released, due next Wednesday (March 20)
  • Goals for today: continue to become more familiar with OCaml, be able to implement a simply-typed lambda-calculus and type-checker in OCaml

More OCaml

  • I collected up some industrial applications of OCaml since there was some interest last time:
    • Facebook messenger currently is implemented in about 50% Reason, which is an alternative OCaml front-end with JavaScript-like synax, see this blog post
    • There is a list of companies here that currently use OCaml, along with descriptions of their use-cases
  • There are a number of languages that are very similar to (or directly inspired by) OCaml that are quite widely used today
    • F# is a .NET langauge that is very similar to OCaml
    • The initial Rust compiler was written in OCaml and the language has many structural similarities

Writing and running code in OCaml files

  • For this week’s homework, you can feel free to work in the online editor and upload a main.ml file upload that file directly to gradescope.
  • For this class, all our assignments will be runnable in this online editor.
    • However, if you want to use more advanced IDE features like autocomplete, you are encouraged to set up your local development environment.
  • OCaml code is defined in .ml files. OCaml files behave slightly differently than working in the top-level: you are not required to terminate each expression with a double-semicolon
  • Note: Be sure to check the types of functions and make sure that they match the specification. If you change any types, you will get a build error from the autograder.
    • If you do get build errors that you cannot diagnose, include a link to the Gradescope submission in any questions so we can help diagnose.

image

  • You may need to zoom out slightly on your laptop to see both editing panes at once

Products and sums

  • OCaml has support for pairs:
> (10, "hello");;
- : int * string = (10, "hello")

> fst (10, "hello");;
- : int = 10

> snd (10, "hello");;
- : string = "hello"

(* OCaml has lightweight syntax for pattern matching on pairs built into let,
   which is extremely convenient *)
> let (fst, snd) = (10, "hello") in fst;;
- : int = 10

(* sometimes you want to disregard one (or more) of the possible pattern matches.
   this can be done using the wildcard `_`: *)
> let (fst, _) = (10, "hello") in fst;;
- : int = 10
  • OCaml has support for variants (aka. algebraic data types aka. sum types):
(* 
  In Plait, we defined an animal type as:

  (define-type Animal
    (tiger [color : Symbol]
           [stripe-count : Number])
    (snake [color : Symbol]
           [weight : Number]
           [food : String])) 

  Let's define an analogous type in OCaml
*)

> type animal =
    Tiger of string * int
  | Snake of string * int * string;;
- type animal = Tiger of string * int | Snake of string * int * string

(* some comments on how this definition differs from Plait:
   - OCaml doesn't have a Symbol type, so we use string instead
   - OCaml doesn't have an equivalent to Plait's Number type, so we use integers instead
   - Data members of the variant in OCaml are not named
   - There is case sensitivity: the name of the type itself must be lower-case,
     and the name of each variant must begin with a capital letter
*)

(* now we can create animals using the following syntax: *)
> Tiger("brown", 7);;
- : animal = Tiger ("brown", 7)

(* aside: this is our first time seeing the `type` keyword, so it's worth noting that 
   we can also use this keyword to define type-aliases, just like the `type-alias` 
   keyword in Plait *)
> type pair_of_bools = bool * bool

(* unlike in Plait, OCaml does not generate accessor functions for you: you *must*
   use pattern matching to destruct variants *)
> let get_color a = 
  match a with (* notice: unlike type-case, we do not need to provide the type we are matching on *)
  | Tiger(color, _) -> color
  | Snake(color, _, _) -> color
val get_color : animal -> string = <fun>

> get_color (Tiger("brown", 10));; (* notice: we needed to use parenthesis here, because Tiger
                                      is a function of type string * int -> animal, and OCaml's
                                      function associating rules bind to the left. If you don't
                                      get this, don't worry; just use more parenthesis! *)
- : string = "brown"

(* we can also compare variants for equality *)
> Tiger("green", 7) = Tiger("green", 7);;
- : bool = true

> Tiger("green", 7) = Snake("green", 7, "mice");;
- : bool = false
  • In-class exercises:
    • Make a function all_green l of type animal list -> bool that returns true if every animal’s color is equal to "green". Use only pattern matching. Hint: don’t forget your parenthesis!
    • Make a function filter_green l of type animal list -> animal list that returns a list of all the green animals in l.

Testing and exceptions

  • The simplest way of testing in OCaml is to use assertions
  • OCaml has a built in function assert v of type bool -> unit that causes a runtime error if v = false.
> assert (10 = 20);;
Exception: Assert_failure ("//toplevel//", 1, 0).
  • Another way to raise an error in OCaml is to use an exception. We will learn (much) more about exceptions later in the course, but for now it is useful to know how to raise them and define them:
(* we declare a new exception using the following syntax: *)
> exception My_exception of string;;

(* then, we can *raise* an exception using the `raise` keyword: *)
> raise (My_exception "oops");;
Exception: My_exception "oops".
  • We will be discussiong exceptions in much more detail later on in this course, but let’s briefly see how OCaml’s exception-handling works
  • Exceptions can be handled using a try <e> with <e> block:
> exception My_exception of string;;
    
> let my_weird_function () =
  try 
    (* run code that might raise exceptions *)
    (raise (My_exception("oops")))
  with 
    (* handle any exceptions *)
    My_exception(s) -> "hello";;

> my_weird_function () ;;
- : string = "hello"
  • Using the try...with block, we can write our own assertions to detect when exceptions are raised (equivalent to Plait’s try/exn function):

  • I encourage you to package up all of your assertions into a function that gets run whenever you execute your code. You can do this as follows:

(* by default, OCaml will run the body of all expressions of the form 
  `let () = e`; think of this like OCaml's version of `main`. You can have
  more than 1 expression of the form `let () = e`; they will be run in 
  sequence *)
let () = 
  assert (1 = 1);
  assert (2 = 2)
  • This is our first time seeing OCaml’s semicolon ; operator. Think of ; like begin in Plait: whenever you want to sequence together computations of type unit, you can put a semicolon between them.
  • A place where the semicolon is often used is in printint output, which is quite useful during debugging:
let () = 
    print_string "hello";
    print_newline ();
    print_int 10;;
hello
10

Records

  • Records are a convenient compound data-type whose members are named:
> type zoo = { zookeeper_name: string; animals: animal list }

(* then, to create a zoo, use the following syntax (note that OCaml infers that
   the type here is `zoo`): *)
> let my_zoo = { zookeeper_name = "steven"; animals = [Tiger("green", 7)]};;

(* now we can access data members of a record using the following syntax: *)
> my_zoo.zookeeper_name
- : string = "steven"
  • It is often useful to use records instead of pairs when you want to reference data elements by their name
  • You can also use records inside of variants if you want to name the members, which can be quite useful:
> type btree =
    Leaf of int
  | Node of { v: int;  left: btree; right: btree };;

(* constructing a record-in-a-variant: *)
> let my_tree = Node { v = 10; left = Leaf(5); right = Leaf(6) } ;;
- : btree = Node {v = 10; left = Leaf 5; right = Leaf 6}

(* pattern matching syntax with records works as follows *)
> let get_value t = 
  match t with
  | Leaf(n) -> n
  | Node {v=v; left=left; right=right} -> v

> get_value my_tree ;;
- : int = 10

Using hashmaps

  • The syntax for using hashmaps in OCaml is a bit more involved than Plait
  • For now, we won’t go into too many details on it: we might have time to discuss it in more detail as the course goes on. If you want to know more, feel free to consult this chapter
  • Here is the syntax for declaring and using hashmaps. I recommend you simply copy and paste this code for now and understand it later:
(* a hashmap in OCaml has a fixed key type that we need to declare ahead of time. 
   In this case, we will make a StringMap that maps strings to values *)
> module StringMap = Map.Make(String);;

> let empty_map = StringMap.empty;;
val empty_map : 'a StringMap.t = <abstr>

(* we insert into the hashmap using StringMap.add <key> <value> <map> *)
> let my_map = StringMap.add "hello" 10 empty_map;;

(* then, we can look up values in the map using the find_opt function
   we will also take this opportunity to demonstrate the OCaml
   `option` type *)
> match StringMap.find_opt "hello" my_map with
  | Some(v) -> v
  | None -> 0

(* you will likely find it useful to have other kinds of keys in hashmaps, for 
   instance integers *)
> module IntMap = Map.Make(Int);;

Mutable state and side-effects

  • Just like Plait, OCaml supports mutable state via boxing, unbox, and set (though, with different syntax):
(* OCaml uses `ref` instead of `box` to create a new boxed value *)
> let x = ref 10;;
val x : int ref = {contents = 10}

(* update the contents of a box using the syntax `x := e`, which has type unit *)
> x := 25;;
- : unit = ()

(* just like Plait, we cannot change the type of a boxed value *)
> x := true;;
Error: This expression has type bool but an expression was expected of type int

(* to dereference, OCaml uses `!e` instead of unbox *)
> !x;;
- : int = 25

(* we can sequence together mutable updates using `;` *)
> let x = ref 25 in
  let y = ref 15 in
    x := !x + 1;
    y := !x - 4;
    !x + !y;; 
- : int = 48

Implementing a $\lambda$-calculus interpreter

  • Now we have all the tools we need to re-implement our friend the simply-typed $\lambda$-calculus in OCaml!
  • See here for an implementation.
  • We use the following abstract syntax:
type typ =
  TNum
  | TFun of typ * typ

type expr =
  Abs of string * typ * expr
  | App of expr * expr
  | Var of string
  | Num of int
  | Add of expr * expr
  • Then, we implement substitution, which looks very similar to Plait:
(* compute e1[x |-> e2] *)
let rec subst e1 x e2 =
  match e1 with
  | Abs(arg, t, e) ->
    if arg = x then Abs(arg, t, e) else Abs(arg, t, subst e x e2)
  | App(app1, app2) -> App(subst app1 x e2, subst app2 x e2)
  | Var(s) -> if s = x then e2 else Var(s)
  | Num(n) -> Num(n)
  | Add(add1, add2) -> Add(subst add1 x e2, subst add2 x e2)
  • Then, we can add an interpreter, which raises a Runtime exception if a run-time error occurrs:
(* a runtime error type that is raised if we encounter an exception during runtime *)
exception Runtime of string

let get_num e =
  match e with
  | Num(n) -> n
  | _ -> raise (Runtime("attempting to get non-number"))

(* interpret lambda term e *)
let rec interp e =
  match e with
  | Abs(s, t, e) -> Abs(s, t, e)
  | Num(n) -> Num(n)
  | Var(s) -> raise (Runtime("unrecognized symbol"))
  | App(e1, e2) ->
    let eval_e2 = interp e2 in
    (match interp e1 with
     | Abs(arg, _, body) ->
       (* run body[arg |-> eval_e2] *)
       interp (subst body arg eval_e2)
     | _ -> raise (Runtime("attempt to eval non-lambda"))
    )
  | Add(e1, e2) ->
    let n1 = get_num (interp e1) in
    let n2 = get_num (interp e2) in
    Num(n1 + n2)
  • Finally, we can add a type-checker, which uses a StringMap as a type environment:
module StringMap = Map.Make(String);;

type tenv = typ StringMap.t

exception Typecheck of string

let rec type_of tenv e =
  match e with
  | Abs(s, t, body) ->
    let new_env = StringMap.add s t tenv in
    TFun(t, type_of new_env body)
  | Num(_) -> TNum
  | Var(s) ->
    (match StringMap.find_opt s tenv with
     | Some(v) -> v
     | None -> raise (Typecheck("unrecognized symbol")))
  | App(e1, e2) ->
    let te2 = type_of tenv e2 in
    (match type_of tenv e1 with
     | TFun(targ, tbody) ->
       if targ = te2 then tbody else raise (Typecheck("invalid function call"))
     | _ -> raise (Typecheck("invalid function call"))
    )
  | Add(e1, e2) ->
    let te1 = type_of tenv e1 in
    let te2 = type_of tenv e2 in
    (match (te1, te2) with
     | (TNum, TNum) -> TNum
     | _ -> raise (Typecheck("adding non-number")))