programming languages and paradigms, j. fenwick, b. kurtz ...blk/cs3490/ch04/ch04.02.pdf ·...

23
Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in F# F# is a multi-paradigm language where it is possible to use features of the functional, imperative, and object-oriented paradigms. In chapters 4 and 5 only the functional features of F# are discussed. The examples presented all involve immutable data; data is immutable if its state cannot be modified after the object is created. Binding allows objects to be “assigned once” but cannot be modified later. This is in contrast to imperative languages where assignments such as x = x + 1 are allowed. By default all data in F# is immutable; mutable values must be declared explicitly. The design of the functional aspects of F# was influenced by objective Caml which, in turn, was influenced by ML (see section 5.2). F# was developed by Microsoft and shares the Common Language Infrastructure (CLI) of the .NET development environment. Other languages supported by CLI are C++, C#, and Visual Basic. It is possible to combine these languages in a seamless manner to produce a single software product. Many of the object-oriented features of F# are shared with C#. When developing an F# application in Visual Studio, you can open a F# Interactive interpreter at the bottom of the screen by pressing Ctrl+Alt+F. You type in F# code at the prompt followed by ;; to force evaluation of the expression. The code fragments shown in this chapter were done in the interpreter.

Upload: others

Post on 15-Aug-2020

5 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

4.2 Programming in F# F# is a multi-paradigm language where it is possible to use features of the functional, imperative, and object-oriented paradigms. In chapters 4 and 5 only the functional features of F# are discussed. The examples presented all involve immutable data; data is immutable if its state cannot be modified after the object is created. Binding allows objects to be “assigned once” but cannot be modified later. This is in contrast to imperative languages where assignments such as x = x + 1 are allowed. By default all data in F# is immutable; mutable values must be declared explicitly. The design of the functional aspects of F# was influenced by objective Caml which, in turn, was influenced by ML (see section 5.2). F# was developed by Microsoft and shares the Common Language Infrastructure (CLI) of the .NET development environment. Other languages supported by CLI are C++, C#, and Visual Basic. It is possible to combine these languages in a seamless manner to produce a single software product. Many of the object-oriented features of F# are shared with C#. When developing an F# application in Visual Studio, you can open a F# Interactive interpreter at the bottom of the screen by pressing Ctrl+Alt+F. You type in F# code at the prompt followed by ;; to force evaluation of the expression. The code fragments shown in this chapter were done in the interpreter.

Page 2: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

In many familiar programming languages the placement of the source code on the page does not affect the meaning of the code. The C language studied in chapter 3 is not dependent on the layout of the statements in the language (although programmers are encouraged to observe “good indentation” to make the code more readable). Languages like C depend on using enclosing characters, such as { and }, to tell the compiler when blocks begin and end. The designers of F# made a different choice; placement of source code on the page is used to determine the structure of the code. This decision forces programmers to use standard indentation and allows the omission of enclosing characters; this results in compact and very readable code. But the beginning programmer in F# more familiar with free form languages may experience some initial frustration when correcting indentation errors. 4.2.1 Primitive Data Types There are a wide variety of numeric types available. When assigning a literal value, the suffix can be used to indicate the desired type, such as: > let pi = 3.14159265M;;

val pi : decimal = 3.14159265M

Table 4.1 summarized the numeric primitives.

Unsigned range (suffix) Signed range (suffix)

byte 0..255 (uy) -128..127 (y)

int (16 bit) 0..65,535 (us) -32,768..32,767 (s)

int (32 bit) 0 .. 232 – 1 (u) -231 .. 231 - 1

int (64 bit) 0 .. 264 – 1 (UL) -263 .. 263 – 1 (L)

float (64 bit) IEEE64, aprx. 15 digits

float (32 bit) IEEE32, aprx. 7 digits (f)

decimal Fixed point, exactly 28 digits (M)

Table 4.1 Numeric Primitives The arithmetic operations include addition (+), subtraction (-), multiplication (*), division (/, remainder discarded for integers), modulus (%, remainder for integer division), and power (**, base must be float or float 32, integer exponents must be explicitly converted to float). Of course there are a whole range of math functions available, including abs, ceil, exp, floor, sign, log (natural), log10, sqrt, sin, cos, tan, pown (integer exponent). Primitive integers support bitwise operations: &&& (bitwise and), ||| (bitwise or), ^^^ (bitwise exclusive or), <<< (shift left), and >>> (shift right). Character literals in F# are enclosed in single quotes. Some characters require use of an escape sequence: single quote (\’), double quote (\”), backspace (\b), backslash (\\), newline (\n), return (\r), and tab (\t). You can convert a character value to other types, as seen here in interactive F#: > int 'A';;

val it : int = 65

> 'A'B;;

val it : byte = 65uy

Page 3: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

String literals are enclosed in double quotes, such as “hello world”. Stings are zero-based and individual characters can be indexed using the .[<index>] notation. Here is a sample interaction in the interpreter: > let str = "hello world";;

val str : string = "hello world"

> str.[0];;

val it : char = 'h'

> str.[10];;

val it : char = 'd'

The Boolean type (bool) only has two values: false and true. The three Boolean operators are And (&&), Or (||), and Not (not). Here is a sample interaction: > let a = true;;

val a : bool = true

> let b = false;;

val b : bool = false

> let c = a && not b;;

val c : bool = true

> let d = (not a) || b;;

val d : bool = false

As with most modern programming languages, evaluation is “short circuited.” In other words, evaluation is from left to right and stops when the final result can be determined. && will stop on the first false value found and || will stop on the first true value. F# has six comparison operators: less than (<), less than or equal (<=), greater than (>), greater than or equal (>=), equal (=), and not equal (<>). The built-in compare function returns -1 (the left operand is less than the right operand), 0 (the left operand equals the right operand), or 1 (the left operand is greater than the right operand) as seen here. > compare 'b' 'c';;

val it : int = -1

> compare 'b' 'a';;

val it : int = 1

> compare 'a' 'a';;

val it : int = 0

4.2.2 Simple Functions and Type Inference F# is a strongly typed language but you do not have to declare your types; the system will infer the types based on usage. Recall the example: > let str = "hello world";;

val str : string = "hello world"

The interpreter was able to deduce that str was of type string. Here is another

example: > let d = (not a) || b;;

val d : bool = false

Page 4: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

The interpreter infers the type bool for the variable d. Only in very rare cases, often involving overloaded functions, the interpreter will ask you to help by specifying the type of a variable. Here is how to tell the system the type: > let str:string = "abc";;

val str : string = "abc"

Look what happens when you give the system a type but the type inferencing mechanism disagrees with your specification.

> let str:string = 'a';;

let str:string = 'a';;

-----------------^^^

stdin(3,18): error FS0001: This expression was expected to have

type

string

but here has type

char

The let binding allows you to define functions. Here are some examples: > let increment x = x + 1;;

val increment : int -> int

The arrow (->) indicates a function type. The name of the function is increment,

the formal parameter name is x, and the body of the function is x + 1. Because the

function body says to add 1, an integer value, to x, the system is able to infer that x

must be of type integer and the function increment has type int -> int. Here is

another example: > let even x = x % 2 = 0;;

val even : int -> bool

> even 5;;

val it : bool = false

> even 6;;

val it : bool = true

Some functions in F# can have generic arguments. For example, the built-in method List.length can return the length of any list of items, regardless of type. To see this, define a function len as follows: > let len L1 = List.length L1;;

val len : 'a list -> int

The notation ‘a or ‘b and so forth are used to indicate generic values who specific type is not known. The minimumofThree function below introduces an if .. then .. else control structure. Here is a function to find the minimum of three integer values. > let minimumOfThree (a:int) b c =

if (a <= b) && (a <= c) then a

else if (b <= a) && (b <= c) then b

else c;;

val minimumOfThree : int -> int -> int -> int

Page 5: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

There are several things that need to be explained in this function definition. Since the <= comparison applies to a wide variety of types the specification of the first parameter a as an int tells the interpreter that all three parameters and the result are of type int.

The nested if statements perform the comparisons expected. The function type, int -

> int -> int -> int, requires some explanation. This is known as curried form:

the first int corresponds to the parameter a, the second int is for the parameter b, the third for parameter c, and the fourth is the return value type. Unlike other languages, F# does not require the keyword return before the return value. The scope of the formal parameters, a, b, c, is limited to the body of the function. Here is a test of the function: > minimumOfThree 5 3 1;;

val it : int = 1

> minimumOfThree 5 1 3;;

val it : int = 1

> minimumOfThree 1 5 3;;

val it : int = 1

4.2.3 Core Types The previous examples have presented several core types: int, float, generics, and functions. In F# functions are “first class citizens,” meaning they can be used just like data objects, such as passed as parameter and returned as function results. Function

types are identified by the -> notation and shown in the interpreter in curried fashion.

The type unit in F# is similar to the void type in C (see chapter 3). Occasionally

functions do not return a value; rather the function is useful because of side effects, such as input or output. Here is the minimumOfThree function modified to print out the smallest value rather than return the smallest value. > let minimumOfThree (a:int) b c =

if (a <= b) && (a <= c) then

printfn "The smallest value is %d" a

else if (b <= a) && (b <= c) then

printfn "The smallest value is %d" b

else printfn "The smallest value is %d" c;;

val minimumOfThree : int -> int -> int -> unit

> minimumOfThree 7 3 5;;

The smallest value is 3

val it : unit = ()

Notice that the printfn function in F# is similar to the formatted print (printf) in C. All programming languages need ways to form aggregates of data. In typed languages there are two styles of aggregations: combining elements of different types and combining elements of the same type. In C elements of (potentially) different types can be combined in a struct; classes perform a similar role in Java and records are used in Pascal, Modula-2, and Ada. In F# aggregates of different types are formed in tuples. A tuple is a comma separated sequence of items enclosed in parentheses. > let data = ("Mary", "Hernandez", 46);;

Page 6: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

val data : string * string * int = ("Mary", "Hernandez", 46)

> let car = ("Porsche", 2012);;

val car : string * int = ("Porsche", 2012)

The data types are not required to be unique, as seen in the first example. The ‘*’ character is used to separate the fields of a tuple when showing the type. Tuples with two elements are referred to as two tuples, those with three as three tuples, and so on. Two tuples are commonly used to express key, value relationships; F# has two built-in functions to decompose a two tuple. The function fst returns the first item in the two tuple and the function snd returns the second item in the tuple. > fst car;;

val it : string = "Porsche"

> snd car;;

val it : int = 2012

The array structure in programming languages like C is used to hold aggregates of the same type. In the functional part of F# the List type performs a similar role and requires that all items are of the same type. The syntax to form a literal list is a semicolon separated sequence of items enclosed in square brackets. Strong type checking is enforced. > let L1 = [3;6;2;8;5;7;9];;

val L1 : int list = [3; 6; 2; 8; 5; 7; 9]

> let L2 = ['3';'6';'2';'8';'5';'7';'9'];;

val L2 : char list = ['3'; '6'; '2'; '8'; '5'; '7'; '9']

> let L3 = ['3';'6';'2';8;'5';'7';'9'];;

let L3 = ['3';'6';'2';8;'5';'7';'9'];;

----------------------^

stdin(11,23): error FS0001: This expression was expected to have

type char but here has type int

List comprehensions can be used to build complex lists quickly. > [1..10];;

val it : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

> [1..2..10];;

val it : int list = [1; 3; 5; 7; 9]

> ['a'..'f'];;

val it : char list = ['a'; 'b'; 'c'; 'd'; 'e'; 'f']

> [ for a in 1 .. 10 do

yield (a * a) ];;

val it : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]

> [for a in 1 .. 5 do

yield! [ a .. a + 1 ] ];;

val it : int list = [1; 2; 2; 3; 3; 4; 4; 5; 5; 6]

The cons (construct) operator, ::, adds an item to the front of a list. There will be an

error if the item is not of the proper type. > 1 :: 2 :: 3 :: [];;

val it : int list = [1; 2; 3]

Page 7: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

> '1' :: '2' :: '3' :: [];;

val it : char list = ['1'; '2'; '3']

> 1 :: '2' :: '3' :: [];;

1 :: '2' :: '3' :: [];;

-----^^^

stdin(22,6): error FS0001: This expression was expected to have

type int but here has type char

The append operator (@) is used to concatenate two lists together. > let first = [1..3];;

val first : int list = [1; 2; 3]

> let second = [9..-1..7];;

val second : int list = [9; 8; 7]

> let third = first@second;;

val third : int list = [1; 2; 3; 9; 8; 7]

> second@first;;

val it : int list = [9; 8; 7; 1; 2; 3]

When working with any immutable data type in F# it must be remembered that any new data that is created, such as by using :: or @, the new data items are constructed by copying data from the arguments, not by modifying the arguments. The examples above show a list of simple values. You can create lists with more complex objects provided the objects have the same type. Consider the following list of tuples: > let children = [("Maria", 12); ("Jose", 9); ("Juanita", 5)];;

val children : (string * int) list =

[("Maria", 12); ("Jose", 9); ("Juanita", 5)]

> List.head children;;

val it : string * int = ("Maria", 12)

> snd (List.head children);;

val it : int = 12

> fst (List.head (List.tail children));;

val it : string = "Jose"

There needs to be functions to decompose a List into component parts. A list can be decomposed into the head of a list (the first item in the list) and the tail of the list (the original list with the first item removed). These methods are from the List module, as seen here: > List.head [1..5];;

val it : int = 1

> List.tail [1..5];;

val it : int list = [2; 3; 4; 5]

> List.head ['h';'e';'l';'l';'o'];;

val it : char = 'h'

> List.tail ['h';'e';'l';'l';'o'];;

val it : char list = ['e'; 'l'; 'l'; 'o']

Page 8: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

It is sometimes convenient to be able to find the nth item in a list; there is a built-in List function to do this. > List.nth [1..10] 7;;

val it : int = 8

> List.nth ['f';'u';'n'] 2;;

val it : char = 'n'

Notice that the position in a list starts at 0 for the first item. Sometimes operations on data will not produce valid results; F# provides an Optional type to handle situations like this. For example, consider the “divide by zero” problem. Here is what happens if this special case is not anticipated: > let div x y = x / y;;

val div : int -> int -> int

> div 11 2;;

val it : int = 5

> div 4 0;;

System.DivideByZeroException: Attempted to divide by zero.

at <StartupCode$FSI_0036>.$FSI_0036.main@()

Stopped due to error

The div function can be redefined to protect the user from throwing an exception by using the Option type. > let div x y =

if y = 0 then None

else Some(x/y);;

val div : int -> int -> int option

> div 11 2;;

val it : int option = Some 5

> div 4 0;;

val it : int option = None

4.2.4 Input and Output You have already seen the use of printfn for simple formatted output. Here is an instructive example that prints out a boolean truth table for a specified function (note: this example was borrowed from Programming in F# by Chris Smith). Notice how a logical function is passed in as a parameter and applied as the function executes. let printTruthTable f =

printfn " |true | false |"

printfn " +-------+-------+"

printfn " true | %5b | %5b |"

(f true true) (f true false)

printfn " false | %5b | %5b |"

(f false true) (f false false)

printfn " +-------+-------+"

printfn ""

();;

Page 9: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

val printTruthTable :

(bool -> bool -> bool) -> unit

The system was able to determine the signature of the function parameter (bool -> bool -> bool) through the application of the function. Here are the truth tables for the “and” and “or” relations. > printTruthTable (&&);;

|true | false |

+-------+-------+

true | true | false |

false | false | false |

+-------+-------+

val it : unit = ()

> printTruthTable (||);;

|true | false |

+-------+-------+

true | true | true |

false | true | false |

+-------+-------+

val it : unit = ()

Next console input and output is shown using methods from the System library. The following example is self-explanatory. > open System

let main() =

Console.WriteLine("What's your name? ")

let name = Console.ReadLine()

Console.WriteLine("Hello, {0}", name);;

val main : unit -> unit

> main();;

What's your name?

John

Hello, John

val it : unit = ()

File input and output in F# comes from the underlying system classes in the .NET environment. Discussion of File I/O is delayed until it is needed in the case studies in chapters 4 and 5. 4.2.5 Functional Programming in F# Using if .. then .. else is one form of selection between alternatives in F#. The primary mechanism for repetition is to write recursive functions. Consider a recursive function to find the length of a list. The general approach will be: if the list is empty then 0 // the base case

else 1 + length of the tail // the recursive case

Page 10: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

Since taking the tail of a list removes the first item, each successive list will be smaller and the base case is eventually reached. Here is the actual code: > let rec len lst =

if lst = [] then 0

else

1 + len (List.tail lst);;

val len : 'a list -> int when 'a : equality

Notice the use of the keyword rec to tell the F# interpreter that this is a recursive function. Here is a test of the function: > len [5..9];;

val it : int = 5

> len ['a' .. 'z'];;

val it : int = 26

There is a built-in length function in F#, as seen here: > List.length ['a' .. 'z'];;

val it : int = 26

It is often instructive to write these built-in functions on your own first to test your understanding of F#.

Writing your own version of the append function is a bit more challenging. To design recursive functions you must ask yourself: (1) what is the base case that can be solved directly without making a recursive call;

when dealing with list structures, the base case is often an empty list (2) consider the general recursive case; how can you make progress towards the base

case. If the base case involves a list becoming empty, then ask yourself how you can decompose a list to progress step-by-step to the base case. For lists, this often involves splitting a list into the head (the first item in the list) and the tail (the list with the first item removed).

This approach is used to write an append method. Consider the case of appending list L1 containing [1;2;3] and the list L2 containing [7;8]. original data decompose L1

recurse

construct

L1 L2

1 2 3 7 8

head L1 tail L1 L2

1 2 3 7 8

head L1 append (tail L1) to L2

1 2 3 7 8

cons (head L1) to (append (tail L1) to L2)

1 2 3 7 8

Page 11: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

Here is the definition and test of the append function. > let rec append L1 L2 =

if L1 = [] then L2

else (List.head L1) :: append (List.tail L1) L2;;

val append : 'a list -> 'a list -> 'a list when 'a : equality

> append [1;2;3] [7;8];;

val it : int list = [1; 2; 3; 7; 8]

> append ['a' .. 'z'] ['A' .. 'Z'];;

val it : char list =

['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'; 'i'; 'j'; 'k'; 'l';

'm'; 'n'; 'o'; 'p'; 'q'; 'r'; 's'; 't'; 'u'; 'v'; 'w'; 'x';

'y'; 'z'; 'A'; 'B'; 'C'; 'D'; 'E'; 'F'; 'G'; 'H'; 'I'; 'J';

'K'; 'L'; 'M'; 'N'; 'O'; 'P'; 'Q'; 'R'; 'S';'T'; 'U'; 'V';

'W'; 'X'; 'Y'; 'Z']

There is a built-in List function List.append. Exercise 4.2A: Write a boolean function named isMember that determines if a given item is contained in a list of items of the same type. 4.2.6 Using Pattern Matching F# provides pattern matching that often makes it easier to write recursive functions. Pattern matching allows you to decompose a list into named components in a single step. For example when you pattern match the list [1;2;3] with hd::tl then hd is bound to 1 and tl is bound to [2;3]. The selection construct that uses pattern matching is called, appropriately, match. Here is a rewrite of the append function let rec append L1 L2 =

match L1 with

| [] -> L2

| hd:tl -> hd::(append tl L2);

Here the length function is rewritten using match: let rec len lst =

match lst with

| [] -> 0

| hd::tl -> 1 + (len tl);

Pattern match can also be used with tuples. Suppose you want to write a function called third that given a three tuple returns the third value. Here is that function; it uses an implicit match on the argument passed in: > let third (_,_,thrd) = thrd;;

val third : 'a * 'b * 'c -> 'c

> third ('a',2,"hello");;

val it : string = "hello"

When writing F# functions you should use either if or match or both as seems appropriate. Why would you need both? Consider writing a delete function that

Page 12: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

removes every occurrence of a particular item from a list. First decide how the function would work when called: > delete 3 [1;2;3;4;3;2;1;2;3;4];;

val it : int list = [1; 2; 4; 2; 1; 2; 4]

You need to examine every item in the list, so the base case is when the list becomes empty. You must decompose the list into its head and tail and then decide if you retain the head in the result list or discard it. Using match will help use determine the base case (the list is empty) or the general case of decomposing the list into its head and tail. Once you have found the head, you need to decide whether to discard it (if it is the item to be deleted) or retain it (if it is some other item). This train of thought leads to the following definition. let rec delete item lst =

match lst with

| [] -> []

| hd::tl -> if hd = item then delete item tl

else hd :: delete item tl;;

Exercise 4.2B: Write a deleteFirst function that only deletes the first occurrence of a specified item from a list. Consider generalizing the notion of removing items from a list, but suppose items are to be removed only if the item satisfied a particular criterion. For example, suppose you want to remove all odd integers from a list. The criterion “odd” can be written as a Boolean function: let odd n = n % 2 = 1;

This function must be passed along with the original list into a “removeIf” function to remove all odd values.

removeIf odd [7;6;4;3;1;2;4];;

val it : int list = [6;4;2;4]

You should not be limited to applying the removal criterion to integers; suppose you want to remove all vowels from a list of characters. You will first need to write a Boolean function that determines if a character is a vowel or not. > let isVowel char =

char='a'||char='e'||char='i'||char='o'||char='u';;

val isVowel : char -> bool

So the expected behavior for removeIf is: > removeIf isVowel ['a'..'z'];;

val it : char list =

['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'j'; 'k'; 'l'; 'm'; 'n'; 'p';

'q'; 'r'; 's'; 't'; 'v'; 'w'; 'x'; 'y'; 'z']

In writing the removeIf function itself, it will need to examine each item in the list one at a time and halt when the empty list is reached. Use a match to detect the base case, an empty list, or in the general case decomposing the list into the head (hd) and tail (tl). Apply the function passed as a parameter to the head of the list; if the result is true the item is removed from the list and recuse on the tail and if the result is false, retain the head and cons it onto the result of the recursive call. Here is the definition:

Page 13: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

let rec removeIf func lst =

match lst with

| [] -> []

| hd::tl -> if func hd then removeIf func tl

else hd:: removeIf func tl;;

There are times when you need a very simple function only for a one-time application. The function “odd” defined above may be one such function. Fortunately F# allows us to define inline, anonymous functions that are used once and discarded. The syntax is

fun <params> -> <body>. Here is how such a function can be used to remove odd

numbers from a list of integers: > removeIf (fun num -> num % 2 = 1) [7;6;4;3;1;2;4];;

val it : int list = [6; 4; 2; 4]

Retaining or removing items from a list occurs so frequently F# provides a filter function in the List module. For the filter function the Boolean function passed in is used to retain items when it is true and discard items when it is false. So if you wanted to discard odd numbers you would pass in an anonymous even function to apply to the list.

> List.filter (fun n -> n%2 = 0) [7;6;4;3;1;2;4];;

val it : int list = [6; 4; 2; 4]

Here is an elegant application of the filter function to write a quick sort function in six lines of code:

> let rec quicksort(xs:List<int>) =

let smaller = List.filter (fun e -> e < xs.Head) xs

let larger = List.filter (fun e -> e > xs.Head) xs

match xs with

| [] -> []

| _ -> quicksort(smaller)@[xs.Head]@quicksort(larger);;

val quicksort : List<int> -> int list

> quicksort [13;89;78;67;17;28;37;45;58];;

val it : int list = [13; 17; 28; 37; 45; 58; 67; 78; 89]);

Exercise 4.2C: This quicksort function works correctly provided that all elements of the list are unique. Modify this function to work correctly to quicksort any list, including those with duplicates.

So far the functions discussed have been applied to list structures. Functions often involve arguments or return values that are tuples. Suppose you want to write a function called “zip”; there are two input lists and one output list of tuples. Assume the lists are of the same length (see the exercise for a variation on this assumption). The corresponding items are put together into a single tuple, as seen here:

zip [1;2;3] ['a';'b';'c'];;

val it : (int * char) list = [(1, 'a'); (2, 'b'); (3, 'c')]

Notice that the lists can contain different types since tuples are allowed to have different types. Assuming the lists are the same length you will use a match to decompose the first list, L1, into its head and tail components. The base case is when L1 becomes empty. You first applied the zip function recursively on the two tails and bind this result

Page 14: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

to r. You then put the two heads together into a single tuple and cons it onto the recursive result r. Here is the code:

> let rec zip L1 L2 =

match L1 with

| [] -> []

| hd::tl -> let r = zip tl (List.tail L2)

(hd, List.head L2)::r;;

There is a built-in zip function as seen here: > List.zip [1;2;3] ['a';'b';'c'];;

val it : (int * char) list = [(1, 'a'); (2, 'b'); (3, 'c')]

This version also requires lists of the same length, otherwise an exception is thrown.

Exercise 4.2D: write a version of zip that handles the case of unequal lengths by using optional types. Here is an example of output the function produces: zip [1;2;3] ['a';'b'] would return

[Some(1, 'a'); Some(2, 'b'); null]

Do not make any assumption about which of the two given lists is the longest in the original function call. Exercise 4.2E: write an unzip function that works as shown here > unzip [(1, 'a'); (2, 'b'); (3, 'c')];;

val it : int list * char list = ([1; 2; 3], ['a'; 'b'; 'c'])

DO NOT use the built-in unzip function. Functional languages support higher order functions such as map (apply a function individually to each item in a list and return the list of results) and reduce (apply a function to elements in a list to accumulate a result based on these applications). You will first write versions of these functions on your own then, in the next section, it will be shown how to use similar built-in functions to solve some complex problems.

First explore how a map function should work. > let square n = n * n;;

val square : int -> int

> List.map square [4; 3; 2; 1];;

val it : int list = [16; 9; 4; 1]

It is not required that the list of results be the same type as the original list, as seen in this example where the “even” function is mapped over a list of integers. > map (fun num -> num % 2 = 0) [1..10];;

val it : bool list = [false; true; false; true; false; true;

false; true; false; true]

Since you are working with a single list you will use the “match” approach to writing the function where the first match detects the empty list and returns the empty list result and the second match decomposes the list into its head and tail to construct the recursive solution. Bind the result of calling map recursively on the tail of the list to the variable r. You then apply the function parameter to the head of the list and cons this result onto the recursive result r. Here is the code.

Page 15: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

> let rec map func list =

match list with

| [] -> []

| hd::tl -> let r = map func tl

(func hd)::r;;

val map : ('a -> 'b) -> 'a list -> 'b list

Exercise 4.2F: There is a built-in partition function that works as shown here: > List.partition (fun n -> n % 2 = 0) [6;3;5;9;8;4;2;1;9];;

val it : int list * int list = ([6; 8; 4; 2], [3; 5; 9; 1; 9])

In this example an anonymous even function is applied to each element of the given list of integers. The result is a tuple of two lists. If the function application resulted in a true value, the item is placed in the first list; if the result is false, the item is placed in the second list. The original relative order of the items is maintained and duplicate values are allowed in the original list. Write your own version of the partition function; do not use the built-in function. The next higher order function to be examined will be called reduce. The intent is to pass in a binary function that can be applied to the arguments of a list to accumulate a final result. In mathematics a binary function takes two arguments and produces a single result. Addition and multiplication of integers would be a function from integer x integer integer. In order to accumulate results, you must supply an initial accumulator value. Here are two examples applying the + and * functions to a list of integers. > reduce (+) 0 [1..5];;

val it : int = 15

> reduce (*) 1 [1..5];;

val it : int = 120

The first parameter is the binary function, the second parameter is the initial accumulator value, and the third argument is the list of values. In these two function applications you have: 1 + 2 + 3 + 4 + 5 = 15

1 * 2 * 3 * 4 * 5 = 120

The values are not required to be in any order, so the same results would have been attained with the original list [3; 4; 1; 2; 5]. The items also do not have to be consecutive if sorted, so using the arguments [1; 100; 10] would produce 111 when reduced by addition and 1000 when reduced by multiplication. To understand how to write this function consider the example of reducing 1..5 by addition. Write this out as above but now include the initial accumulator value. 0 + 1 + 2 + 3 + 4 + 5 = 15

Since this is a binary function you need to associate values into groups of two: (((((0 + 1) + 2) + 3) + 4) + 5) = 15

To help understand how to write the algorithm, perform the addition step-by-step. (((((0 + 1) + 2) + 3) + 4) + 5) =

((((1 + 2) + 3) + 4) + 5) =

(((3 + 3) + 4) + 5) =

Page 16: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

((6 + 4) + 5) =

(10 + 5) = 15

By carefully examining this sequence of calculations, you can make the following observations: (1) the first addition was adding the initial accumulator value to the head of the initial list (2) the result of each addition becomes the current accumulator value (3) at each step you add the current accumulator value to the head of the remaining list (4) after the final operation is performed, the final accumulator value is the final result You can use these observations to write the follow psuedocode: if the list is empty return the accumulator value // base case

else // recursive case

apply the binary operation to the accumulator and the

head of the current list to find the new accumulator value

recursively call reduce with the same function, the new

accumulator value and the tail of the current list

Use the first case of the match to detect the empty list and return the accumulator value. For the recursive case decompose the list into the head and tail. Apply the binary function to the accumulator value passed in and the head of the list; this gives the new accumulator value called r. You make the recursive call passing in the function (unchanged), the new accumulator, and the tail of the list.

> let rec reduce func accum list =

match list with

| [] -> accum

| hd::tl -> let r = func accum hd

reduce func r tl;;

Notice that all of the work for this function is done going into recursion. By the time you reach the base case, you have the final answer. This is known as tail recursion and it is implemented very efficiently by the F# runtime system. As you probably learned in data structures, recursion is implemented by stacking a sequence of activation records onto the runtime stack. The F# system is smart enough to detect functions that are written using tail recursion and does not create a stack of activation records. This is possible because the final answer is known when the base case is reached; there is no useful work done when coming out of recursion. In section 5.1 on advanced techniques in F# programming you will learn how to transform an algorithm that is not tail recursive into an equivalent tail recursive algorithm. 4.2.7 Mapping, Iteration, and Folding

The List module in F# has many very powerful built-in functions. Here is an example of

using the built-in map function:

List.map (fun n -> n * n * n) [1..8];;

val it : int list = [1; 8; 27; 64; 125; 216; 343; 512]

Page 17: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

You have already seen a nice example of using the List.filter function to write a

quicksort. You also saw how the List.partition function allowed us to divide a given list

of integers into two lists, one with even integers and one with odd integers.

One function you have not explored yet is List.iter; it is primarily used to apply a

function over a list without modifying the list but having some useful side effect. The

most common side effects are some input or output activity. Consider the earlier

example with a list of children.

> let children = [("Maria", 12); ("Jose", 9); ("Juanita", 5)];;

val children : (string * int) list =

[("Maria", 12); ("Jose", 9); ("Juanita", 5)]

The goal is to print out a list of names for all the children. To do this you define a printName function that is passed in a child tuple and prints the first item which is the child’s name. > let printName child = printf "%s " (fst child);;

val printName : string * 'a -> unit

Notice how effective the type inference system is in this case. There is no type given for child, but because the fst operation was applied to child, the system infers that child must be a tuple. Furthermore, since the format part of the printf used %s (indicating a string) the system infers that the first argument in the tuple must be a string. You now use the built-in List.iter function to apply the printName function to the list of children. After applying the List.iter, you can print out the list of children to show that the values were not changed in the original list. > List.iter printName children;;

Maria Jose Juanita val it : unit = ()

> children;;

val it : (string * int) list =

[("Maria", 12); ("Jose", 9); ("Juanita", 5)]

The built-in function List.fold is similar to the reduce function you wrote in the previous section. Here are two applications of List.fold that should look familiar: > List.fold (+) 0 [1..5];;

val it : int = 15

> List.fold (*) 1 [1..5];;

val it : int = 120

Exercise 4.2G: Suppose as a result of some function operation you end up with a list of

boolean values, such as [true,false,false,true,true]. You want to reduce the

list of boolean values by applying one of the boolean functions (And, Or) to the entire

list. Using fold this can be done in one line of code. Write the code for the && operation

and the code for the || operation.

To appreciate the real power of the fold operation, consider a nontrivial example of fold

that borrowed from “Programming in F#” by Chris Smith. The goal is to count the

Page 18: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

number of each of the vowels (‘a’, ‘e’, ‘i’, ‘o’, ‘u’) in a given string. Here is the complete

code and application of the function.

let countVowels (str : string) =

let charList = List.ofSeq str

let accFunc (As, Es, Is, Os, Us) letter =

if letter = 'a' then (As + 1, Es, Is, Os, Us)

elif letter = 'e' then (As, Es + 1, Is, Os, Us)

elif letter = 'i' then (As, Es, Is + 1, Os, Us)

elif letter = 'o' then (As, Es, Is, Os + 1, Us)

elif letter = 'u' then (As, Es, Is, Os, Us + 1)

else (As, Es, Is, Os, Us)

List.fold accFunc (0, 0, 0, 0, 0) charList;

countVowels "now it the time for all good men";;

val it : int * int * int * int * int = (1, 3, 2, 4, 0)

The final two lines show that the string "now it the time for all good men" has 1 ‘a’, 3 ‘e’,

2 ‘i’, 4 ‘o’, and 0 ‘u’. The first line of the function definition converts the given string into

a list of characters by using the List.ofSeq function. The accFunc is passed in a tuple of

counts for the five vowels and the letter currently being considered. Notice how the

formal parameter in the function definition decomposes the tuple into the individual

counts. F# provides a multi-alternative if structure with the following syntax:

if (condition1) then (action1)

elif (condition2) then (action2)

: : :

else (default action)

Each of the vowels are checked in turn, and if a vowel is found, the count for that vowel

is incremented by one. If no vowels are found, a tuple with the input values is returned.

The last step is to call the List.fold passing in the accFunc function, the initial

accumulator values (all zeros) and the list of characters that is being folded. So the

accFunc is called for each of these characters, one at a time, and the counts of the

vowels in the tuple are changed or left alone based on the current character being

examined.

Exercise 4.2H: Rewrite the function accFunc in countVowels to use a “match” on the

letter instead of the nested if structure. Test you function well, including limiting cases

such as having no vowels at all.

Page 19: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

There is an alternative fold operation called List.foldback that sometimes makes it

possible to write more efficient code.

When you combine these built-in higher order functions in F# you start appreciating the

computational power and brevity of functional languages. Consider the problem of

having a list of purchases and you want to calculate the total tax collected. First map

the tax function over the list then add it up. Here is the function definition:

> let taxCollected purchases taxRate = List.fold (+) 0.0

(List.map (fun item -> item * taxRate) purchases);;

val taxCollected : float list -> float -> float

Now test the function in the interpreter:

> let purchases = [7.98; 15.67; 24.32; 8.75]

let taxRate = 0.08

printfn "Tax collected = %f" (taxCollected purchases taxRate);;

Tax collected = 4.537600

4.2.8 Defining Your Own Data Types and Discriminate Unions Defining a discriminate union is a useful technique to build objects out of named data types. You will investigate three examples related to data structures: a single linked list, a binary search tree, and an ordinary binary tree. You first define a Single Linked List (SLList) datatype: type SLList =

| Node of int * SLList

| Empty

Entering this type definition in the interpreter will return the definition itself. This must be done before using the type in function definitions. A Node is a tuple composed of an integer value and a reference to another SLList type. Notice the recursive nature of the data type. The other alternative for a SLList is that it is Empty. Given the following conceptual list:

1 2 3 4 Empty

Using the SLList data type, the representation of this list in F# would be: Node(1, Node(2, Node(3, Node(4,Empty))))

You now want to define a function that adds new values to an SLList; the addition will always be at the end of the list. Here is the recursive function. > let rec add value sllist =

match sllist with

| Empty -> Node(value, Empty)

| Node(v, link) -> Node(v, add value link);;

Page 20: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

val add : int -> SLList -> SLList

The parameters are the integer value you want to add and the current linked list. Either the list is initially empty, in which case you create a single node containing the value and an empty reference or the list is not empty, in which case you must recurse down the list until the empty reference is encountered in the last node. The nodes that are passed during this traversal must remain unchanged. So given the current node of the

form Node(v, link)you leave the value v intact and apply the add function

recursively to the link field. Eventually you reach the base case of the empty link where you insert in its place a node with the new value and the empty link. To test this function add the value 2 to an empty list, then the value 4, then the value 1, and finally the value 3. Since each new value is always added at the end, you would expect the following conceptual result.

2 4 1 3 Empty

You need to nest the function calls so that the first add of 2 to the empty list is the most nested function call. Here is the complete sequence of function calls and the result. > let L1 = add 3 (add 1 (add 4 (add 2 Empty)));;

val L1 : SLList = Node (2,Node (4,Node (1,Node (3,Empty))))

Exercise 4.2I: You will use the same data type definition for SLList but you will write an addInOrder function that places the new value in ascending order in the list. You may assume the list parameter passed into the addInOrder method is properly sorted. Duplicates are allowed. Here is a sample call to this method and the result that should be produced: > let L1 = addInOrder 3 (addInOrder 1 (addInOrder 4

(addInOrder 2 Empty)));;

val L1 : SLList = Node (1,Node (2,Node (3,Node (4,Empty))))

The next data type will be a binary search tree. Here is the definition of the type: type BinarySearchTree =

| Node of int * BinarySearchTree * BinarySearchTree

| Empty

Other than the name of the type, there is nothing in this definition that is unique to a binary search tree. This could be any binary tree containing integer values. It is how values are inserted into a binary search tree (BST) that makes it unique from other binary trees. Assume the BST contains unique key values. The BST property states that at every node in the tree, the left subtree contains only key values less than the key value at the node and the right subtree contains only key values greater than the key value at the node. The BST is a sorted tree meaning that an in order traversal of the tree will visit the items in the tree in key value order.

Page 21: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

The next task is to write a bstAdd function that adds values to the tree so that the BST property is maintained. If the value being added is already in the tree, then the tree remains unchanged. Recall that if a new value is added to a BST, then that value is always a leaf in the tree after a search for the value results in an empty reference. You will use a match on the discriminate union; this will result in two alternatives, a Node or Empty. If the Empty value has been reached, you return a new Node with the key value and two empty subtrees. If you have not reached the place for insertion, you proceed to search the left subtree or the right subtree based on the key value. If the value you are searching for is already in the tree, then the tree is returned unchanged. > let rec bstAdd value bst =

match bst with

| Node (data, left, right)

-> if value < data then

Node(data, (bstAdd value left), right)

elif value > data then

Node(data, left, (bstAdd value right))

else bst

| Empty -> Node(value, Empty, Empty);;

val bstAdd : int -> BinarySearchTree -> BinarySearchTree

Now test the bstAdd function by adding the nodes 6, 8, 4, 7, and 5 to the tree in that order. Since 6 is added first it is the most deeply nested function call. > let T1 = bstAdd 5 (bstAdd 7 (bstAdd 4 (bstAdd 8

(bstAdd 6 Empty))));;

val T1 : BinarySearchTree =

Node

(6,

Node (4,

Empty,

Node (5,Empty,Empty)),

Node (8,

Node (7,Empty,Empty),

Empty))

The response shown above has been formatted so that you can see the structure of the

binary search tree.

You will complete this investigation of a BST by writing an in order traversal of the tree.

The visit to each node will print out the node value on a separate line.

> let rec traverse tree =

match tree with

| Node (data, left, right)

-> traverse left

printfn "Node %d" data

traverse right

| Empty

-> ();;

val traverse : BinarySearchTree -> unit

Page 22: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

When you apply traverse to the tree T1 defined previously, the nodes are printed in sorted order, as expected. > traverse T1;;

Node 4

Node 5

Node 6

Node 7

Node 8

val it : unit = ()

Exercise 4.2J: Modify the data stored in the BST to be a tuple containing three fields: an

integer partID that is assumed to be unique, a string partName, and a decimal number

representing the price. Modify the code given above to create a BST of parts that

maintains the data sorted by the part ID. If the part ID already exists in the tree at the

time of addition, then the remaining data fields should be changed to the given data

fields.

This final example shows how you can define a generic discriminate union. You will use

a binary tree that makes no assumptions on how the data is ordered.

> type BinTree<'a> =

| Node of 'a * BinTree<'a> * BinTree<'a>

| Empty;;

type BinTree<'a> =

| Node of 'a * BinTree<'a> * BinTree<'a>

| Empty

Notice that the F# interpreter responds to the type definition by printing it back out. The following code creates a tree T1 that contains seven string values. The code has been indented to reflect the intended tree structure. > let T1 = Node ("Bob",

Node ("Bill",

Node ("Jill", Empty, Empty),

Node ("George", Empty, Empty)),

Node ("Francis",

Node ("Jane", Empty, Empty),

Node ("Jim", Empty, Empty)));;

val T1 : BinTree<string> =

Node("Bob",Node("Bill",Node("Jill",Empty,Empty),

Node("George",Empty,Empty)),Node("Francis",

Node("Jane",Empty,Empty),Node("Jim",Empty,Empty)))

Now write an in order traversal of the tree passing in a function parameter that will be applied to every node when it is visited.

Page 23: Programming Languages and Paradigms, J. Fenwick, B. Kurtz ...blk/cs3490/ch04/ch04.02.pdf · Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris 4.2 Programming in

Programming Languages and Paradigms, J. Fenwick, B. Kurtz, C. Norris

> let rec inOrder func binTree =

match binTree with

| Empty -> ()

| Node(data, left, right) ->

inOrder func left

func data

inOrder func right;;

val inOrder : ('a -> unit) -> BinTree<'a> -> unit

Finally you test the traversal on the tree T1 you constructed by passing in an anonymous function that prints each value on a separate line. > inOrder (fun value -> printf "%s\n" value) T1;;

Jill

Bill

George

Bob

Jane

Francis

Jim

val it : unit = ()

Exercise 4.2K: Using the generic BinTree, define a tree of parts, as defined in the

previous exercise, and traverse the tree to print the part information in the form:

ID: 34; Name: hammer; Price: 8.99

Your tree should contain five items and there is no requirement that the data be sorted

in any way.