CS17 Integrated Introduction to Computer Science Hughes Lab 7: OCaml 12:00 PM, Oct 22, 2017 Contents 1 Getting Started in OCaml 1 2 Pervasives Library 2 3 OCaml Basics 3 3.1 OCaml Types........................................ 3 3.2 OCaml Syntax....................................... 4 3.3 OCaml Loves Types and So Should You......................... 4 4 Options (Not Optional) 4 5 The Design Recipe, in OCaml! 5 5.1 Design Recipe for Atomic Data.............................. 6 6 List Recursion 7 Objectives By the end of this lab, you will know: ˆ OCaml types ˆ OCaml syntax ˆ how and when to use options By the end of this lab, you will be able to: ˆ use Atom and compile OCaml code on the command line ˆ write recursive procedures based on built-in recursive types like lists ˆ write recursive procedures based on user-defined variant types ˆ pattern match in Ocaml
1 Getting Started in OCaml So far in CS 17, we ve written and run all our code in an Integrated Development Environment, or IDE. This means that we could run our code within the window that we were writing it in. But sadly, as we move on to OCaml, we have to say good bye to our friend DrRacket. Instead, we will be writing our code in the text editor Atom, and then running it from the command line! To get started, open up the terminal, and type atom & at the prompt. Wait a few seconds for Atom to load, and then mouse over the Atom window to activate it. Note: Before we move on, please configure your Atom to allow for OCaml syntax highlighting. Look at the OCaml installation guide on the website for help. Now you can either open an existing OCaml file or create a new one. Since you don t yet have any existing OCaml files, let s go ahead and create one. Type in the following lines of code: let a = 17 ;; let b = a ;; a + b ;; To save your program to a file, type Ctrl s. Give it an informative name, such as test.ml. (All OCaml files should end with the extension.ml.) You ll notice at this point that the colors of your text changed! This happened because Atom has intelligent syntax highlighting, meaning the editor will informatively color different parts of your program to make it easier for you to code and debug. Now, let s return to your terminal window and run your program. Note that in order to run your program from the command line, you need to first navigate into the directory in which your file is saved. 1. To check if your code has any syntax errors, you can run ocaml test.ml. If your code is error-free, this will output nothing! Otherwise, it provides a useful way to hunt down syntax errors in your code before moving on. 2. You can then run your code like this: ocaml < test.ml. Use this option when you want to see what your program outputs (e.g. the results of your test cases). 3. Alternatively, you can run the OCaml REPL, or Read-Evaluate-Print-Loop, yourself, by entering ocaml on the command line. Then, type #use "test.ml" ;; - this will read your program and evaluate each top level expression. You can also type OCaml procedures directly into the REPL to see what they will output. Don t forget the quotation marks (or the two semi-colons)! To exit from the OCaml interpreter, type either Ctrl-d or exit 1 ;;. Going back to your test.ml file, let s try entering a bad program. We might, for example, refer to a procedure that doesn t yet exist: factorial 5 ;; 2
After typing this in, let s check for syntax errors by entering ocaml test.ml into the terminal. You should now see an error message showing what caused the problem. You should also see a line indicating which line contains the error. Let s try something else, like making a spelling mistake: let five = 5 ;; fvie + 6 ;; Again, you will see a similar error message, but at the end, it will also ask you Did you mean five Whenever you finish writing a program, make sure to save it, and then exit Atom by closing your Atom window (Ctrl q). 2 Pervasives Library OCaml has lots of procedures and types predefined for you to use. Most of them are contained in libraries. To make use of procedures defined in libraries, you need to use a directive, a command that tells OCaml to import the library. There is one exception, however: the Pervasives library is preloaded when you start OCaml, so the definitions in that library are immediately accessible. The Pervasives Library, in essence, contains the procedures that you can use without defining them yourselves ( OCaml Builtins, so to speak). You can find out which procedures are defined for you here: http://caml.inria.fr/pub/docs/manual-ocaml/libref/pervasives.html Note: When you start up OCaml, you will see what looks like an error message, and says The files /course/cs017/src/ocaml/cs17setup.cmo and /local/bin/ocaml disagree over interface Pervasives. This is because we have modified some procedures (like + and * ) so that their behavior is safer for people new to programming. You can safely ignore this message. 3 OCaml Basics Before we begin, make sure to include our OCaml setup at the top of all of your assignments to make sure you have access to all of our functions. To get the CS17 OCaml version of a teachpack, first run the following command from the directory in which you are working: cp /course/cs0170/src/ocaml/cs17setup.ml. This will copy the teachpack to your current directory. Then include the following line at the top of your file #use "CS17setup.ml" ;; Note that you will need to do this at the top of all of your OCaml assignments! 3
3.1 OCaml Types Task: For each OCaml expression below, state the type of its value (not its value). For example, 3 has the value 3, but the type int. Some other OCaml types include float and string. If there is a type clash, meaning the expression s type is not well-defined, explain why. Please use the OCaml interpreter only to check your answers. 1. 3 2. 3.0 3. 3 + 3.0 4. 3 / 2 5. fun x ->x / 2.0 6. [1; 2; [3; 4]] 7. (17, ["shiba"; "labrador"; "pug"; "corgi"], (false, true)) 8. [(1, 2, []); (3, 4, [5; 6; 7])] 9. if test then 1 else false 10. let rec count_pupper alod = match alod with [] - > 0 "pupper" :: tail - > 1 + count_pupper tail _ :: tail - > count_pupper tail 3.2 OCaml Syntax Task: Below, we have given you some expressions in Racket. Convert them to OCaml, and then execute them by running ocaml to ensure that your code is correct. When writing fibonacci, you may find it useful to use the OCaml when keyword, which checks a condition in a match case. An example of when usage can be found in the code for factorial in the next section of the lab. (+ 17 18) (* (+ 19 21) ( - 4 3)) (define (list - length alod) (cond [(empty? alod) 0] [(cons? alod) (+ 1 (list - length (rest alod)))])) (define (fibonacci x) (cond [(<= x 1) 1] [(> x 1) (+ (fibonacci ( - x 1)) (fibonacci ( - x 2)))])) 4
3.3 OCaml Loves Types and So Should You Unlike Racket, OCaml is a statically typed language. This means that OCaml will produce an error or refuse to read and print if the argument passed does not match the expected type. Clarifying your types helps the user of your function to know what types of arguments to put in and what order they go. This is analogous to the type signatures you wrote in Racket. For example: let rec factorial (x: int) : int = match x with x when x <= 1 - > 1 _ - > x * factorial (x - 1) ;; requires that you give factorial an int. If you give it a float, or any other type for that matter, it will throw an error. Note: We require you to type annotate all of your aguments and return types. 4 Options (Not Optional) Options are an important example of a parameterized variant type that is built in to OCaml: type 'a option = None Some of 'a In words, an 'a option is either a None or a Some applied to an argument of type 'a. Options are useful when writing a procedure that might not produce a value, such as a procedure that looks up a word in a dictionary, since that word might not be in the dictionary! Formally, a dictionary is a set of key-value pairs. For instance, in an English language dictionary, the keys are words and the values are the corresponding definitions. If you look up a word that is in the dictionary, then what you find is the corresponding definition. But if you look up a word that is not in the dictionary, then you don t find anything at all. What should a procedure that looks up keys in a dictionary produce when the key is not found? One possibility is the string "key not found". But what if there is a word in the dictionary whose definition is precisely "key not found"? If such a definition is possible, how could you distinguish between a successful and an unsuccessful lookup? A better alternative for when lookup fails is to produce a special value that cannot possibly be confused with a definition this is exactly what options allow you to do. Here is a data definition for dictionaries: type ('a, 'b) dict = ('a * 'b) list ;; (* Examples of dict *) [(1, 1); (2, 4); (3, 9); (4, 16)] ;; [(1, "one"); (2, "two"); (3, "three")] ;; 5
Task: Write a procedure, lookup, which consumes a dictionary and a key, and produces an option on the corresponding value. Examples: lookup [(1, 1); (2, 4); (3, 9); (4, 16)] 3 => Some 9 lookup [(1, "one"); (2, "two"); (3, "three")] 2 => Some "two" lookup [(1, "one"); (2, "two"); (3, "three")] 4 => None You ve reached a checkpoint! Please call over a lab TA to review your work. 5 The Design Recipe, in OCaml! In this part of the lab, we will review the design recipe for atomic data and the design recipe for mixed data, applying the exact same recipe as before while programming (the exact same procedures as before). 5.1 Design Recipe for Atomic Data The design recipe carries over more or less exactly from one programming language to another. The only difference between applying it to OCaml, as compared to Racket, is that the type signature does not appear in a comment. Instead, it appears as actual code. In particular, the call structure of every procedure you write in OCaml should be annotated with the types it consumes and the type it produces. For example, here is the call structure together with the type signature for the flip procedure in OCaml: let flip (alop : string*string list) : string*string list =... As for testing, your old friends check_expect, and a new tool, check_within are available for all your testing needs! You can use check_within to check the value of floats. To use check_within, pass in the procedure and its arguments, the expect result, and the tolerance, i.e. the decimal value to which we want to test the accuracy. In OCaml, they have underscores instead of hyphens, and are used like this: check_expect actual expected check_within actual expected within For example, here s the flip procedure in OCaml, developed by following the design recipe. Notice how much shorter it is than its Racket counterpart, because we can use tuples! 6
#use "CS17setup.ml" ;; (* Inputs: a list of pairs of strings, alop *) (* Output: a list where the ith element is in the same position, but the order of its elements is reversed *) let rec flip (alop : (string*string) list) : (string*string) list = match alop with [] - > [] (a, b) :: tail - > (b, a) :: (flip tail) ;; "Test cases for flip" ;; check_expect (flip []) [] ;; check_expect (flip [("hello", "world")]) [("world", "hello")] ;; check_expect (flip [("i", "love"); ("ocaml", "programming")]) [("love", "i"); ("programming", "ocaml")] ;; Task: Follow the design recipe to implement a procedure that converts degrees in Fahrenheit to degrees in Celsius. Hint: C = 5 9 (F 32), where C denotes degrees in Celsius, and F, degrees in Fahrenheit. You ve reached a checkpoint! Please call over a lab TA to review your work. 6 List Recursion In programming, as in natural languages, when one learns a new language, the tendency at first is to try to write programs in an old language, and then translate those programs into the new one. Sometimes this fails outright; more often it is possible, but bad style. We d like you to avoid the temptation to write Racket programs in OCaml. So in this problem we are explicitly forbidding the use of List.hd and List.tl, OCaml s equivalent of first and rest. You must use pattern matching instead. Here is the template for writing procedures that recur on lists in OCaml: let rec proc (alod : 'a list): <return - type> = match alod with [] - > head :: tail - >... head... (proc tail)... And here is the familiar length procedure: let rec length (alod : 'a list) : int = match alod with [] - > 0 _ :: tail - > 1 + length tail Task: Write the familiar procedure sum_list in OCaml. This procedure consumes a list of ints and produces the sum of the ints in that list. 7
Task: Rewrite sum_list using List.fold_right (or List.fold_left). 1 Hint: Here s how to get started: let sum_list alon = List.fold_right... Since sum_list applies to a list of integers, its type is int list ->int. Likewise, it is possible to apply List.fold_right to a few, but not all, of its arguments until its type is also int list ->int. Task: Define a type, base, that has one of four values "A", "C", "G", "T". Task: Write a procedure, complement, that takes in a base and returns its complement. Note: "A" and "T" are complements of each other, and "C" and "G" are complements of each other. Task: Write the procedure memberp : 'a * 'a list ->bool, which consumes a tuple consisting of a datum and a list of data, and determines whether that datum is in that list. For example: memberp (3, [1; 2; 3; 4; 5]) => true Note: In OCaml, you cannot use? to distinguish predicates, because the? character is reserved for another use. As a refresher, a predicate is a one-argument procedure that returns a boolean, such as even? or empty? in Racket. An alternative is to append a P to the end of the predicate s name: e.g., memberp. The P stands for predicate. You are at the grocery store, shopping for fruit. You want apples, bananas, and oranges, each of which is only purchaseable in a group (you cannot buy individual fruits). You get to the checkout, and want to know how many items are in your basket. Task: Define a type, fruit, that has an int parameter. There will be three types, apple, orange, and banana. Task: Write a procedure, num-fruits, that takes in a list of fruits and outputs the total number of fruits in the list. For example, a list of apple(3), banana(4) should output the integer 7. Task: Challenge (Time Permitting): Implement fold in OCaml. Once a lab TA signs off on your work, you ve finished the lab! Congratulations! Before you leave, make sure both partners have access to the code you ve just written. Please let us know if you find any mistakes, inconsistencies, or confusing language in this or any other CS17document by filling out the anonymous feedback form: http://cs.brown.edu/ courses/cs017/feedback. 1 You will learn the difference between these two procedures soon enough! 8