Chapter 5 Advanced Techniques in Functional Programmingblk/cs3490/ch05/ch05.02.03slides.pdf · –...
Transcript of Chapter 5 Advanced Techniques in Functional Programmingblk/cs3490/ch05/ch05.02.03slides.pdf · –...
Chapter 5 Advanced Techniques in Functional Programming
Programming Languages and Paradigms J. Fenwick, B. Kurtz, C. Norris
(to be published in 2012)
Introduction • In the remainder of this chapter you will learn
about – Lisp – the first functional programming language – Haskell – a pure functional language designed in the
1990s where data is immutable and functions are not allowed to have side effects
– ML – designed in the 1970s, it introduced type inferencing; F# is in the ML family of languages
• The chapter ends with a case study where you will build an interpreter for Wren Intermediate Code – You will use a record structure to represent the state of
the computation; instructions will be passed in the current state and return a modified state after execution
The List Structure in LISP • A picture of a simple list
• Lisp was originally developed on an IBM 704 – 15 bits of a machine word were called the Contents of
the Address Register, shortened to CAR – Another 15 bits were called the Contents of the
Decrement Register, shortened to CDR – The CAR-CDR pair are said to form a CONS cell – L, I, S, P above are atoms
Accessing a List Structure
• The CAR and CDR functions simply follow pointers – (CAR L1) returns the atom L – (CDR L1) returns the list (I S P) – The (CAR (CDR L1)) returns the atom I; this is
abbreviated as (CADR L1) – What function call would return the list (P)? – What function call would return the atom P? – The List.head and List.tail functions in F# correspond to
the CAR and CDR functions in LISP
Building a List Structure
• The CAR and CDR functions decompose a list • The CONS function builds a list; it forms a CONS cell
– (CONS P () ) would return the list (P) – (CONS S (CONS P () ) ) would return the list (S P) – How would you build the list (L I S P)? – The infix :: function in F# corresponds to CONS;
‘L’ :: ‘I’ :: ‘S’ :: ‘P’ would build the list shown above; in F# lists are required to contain the same type
Decisions in LISP • The conditional function, COND, is used to select
between many alternatives; the general form is: (COND (<predicate1> <task1>) (<predicate2> < task2>) … )
• The first true predicate triggers the corresponding task; for example the absolute value of X would be returned by (COND ((< X 0) (-X)) (T (X)) ); the T in the predicate is True, if reached its task always fires
• The tasks are often function calls, including recursive function calls for named function
• In F# COND has been replaced with if…then…else…
Defining Functions in LISP • The general syntax for defining a function is:
(defun <name> (<params>) <body> ) • The body is often a COND function • The basic mechanism for repetition is recursion • Example: “my-member” returns nil if the item is not
found and returns the remainder of the list starting with the item if it is found
Comparing Simple LISP and F# functions
let append list1 list 2 = if list1 = [] then list2 else List.head list1 :: append (List.tail list1) list2
let length list = if list = [] then 0 else 1 + length List.tail list
Why Tuples are needed in F# • LISP was not a typed language so lists could mix
types; F# is strongly typed and requires lists to contain the same type (like arrays in Java)
• The tuple in F# allows the mixture of types; it is similar to struct in C or records in Ada; Java uses classes for this purpose
• Here is a picture of a tuple in F# ( , , )
“Bill” 43 ‘m’
Intermixing Lists and Tuples - 1 • Recall the List.zip function:
List.zip [‘a’; ‘b’; ’c’] [1; 2; 3] returns [ (‘a’, 1) ; (‘b’, 2); (‘c’, 3) ] a list of tuples
• This structure can be visualized as
,
‘a’ 1
,
‘b’ 2
,
‘c’ 3
Accessing elements, even in a complex structure, is just “pointer chasing”
Intermixing Lists and Tuples - 2 • Assume the function nonNegative returns true if its
argument is >= 0 and false otherwise • Recall the List.partition function:
List.partition nonNegative [1; -5; -2; 3; 6; -1] returns ( [1; 3; 6] , [-5; -2; -1] ) a tuple of lists
• This structure can be visualized as ,
1 3 6 -5 -2 -1
Other Features of Lisp • Supports high level functions
– mapcar applies the function parameter to every item in a list and returns a list of results
– This is the same as Lisp.map in F#
• Typing – Original Lisp is dynamically typed meaning the latest
binding is the current type – Put another way, the identifier itself does not have a
type but the current value of the identifier is typed
Haskell • Characteristics
– Statically typed and uses type inference – immutable data and functions don’t have side effects – Similar to F# in many ways but uses different syntax
• Cons is : (in F# it is ::) • Append is ++ (in F# it is @), and so forth
– Indentation is important (similar to F#) – Supports pattern matching – Supports function currying – Supports list comprehensions – Uses lazy evaluation as the default (as contrasted to F#
where the default is eager evaluation) – The textbook contains many example functions
ML • Characteristics
– Statically typed, statically scope, uses type inference – Supports built-in higher ordered functions like map – Supports function closures – Unlike Haskell and similar to F# in using eager evaluation – Many examples are included in the textbook
• A bit of history – Started with Standard ML, Caml is a descendant – OCaml is Caml with objects – F# is considered to be a descendant of OCaml
Implementing a WIC Interpreter in F# • Will function the same as the C interpreter and has
similar structure • Decompose you program into four modules
– A table implementation module (table.fs) for the jump table, symbol table, and table of instructions
– An instructions module (instructions.fs) with functions for each of the instructions in WIC
– A preprocessing module (preprocess.fs) will produce the initial state of computation
– A main program (interpreter.fs) initializes everything and then interprets the WIC; it is relatively short since the real work is done elsewhere
Importance of Position in Solution Explorer • When using F# in Visual Studio the modules will be
listed at the right side of the screen – Important: each module can only see items in itself and
the modules above it – You cannot see functions or types in modules below – If you right click on the file name in Solution Explorer
you will have options to move the file up or down
Functions for tables • The ST and JT will have type string * int list
– The function retrieve identifier table returns the integer value associated with identifier; throws an exception and message using failwith if the identifier is not found
– The function store identifier value table if the identifier is already in the table then the value is updated to the given value; otherwise a new tuple is formed and added to the table. Store returns the updated table
Functions for tables • The instruction table will have type string * string
list – The first entry in the instruction tupe is the opcode and
the second entry is the operand – If there is no operand, an empty string is stored – The function fetchOpcode location instructions returns the opcode at the specified location; location numbering starts at 0 so use List.nth
– The function fetchOpcode location instructions returns the operand at the specified location; it may be the empty string for many instructions
Lab Activity ch05.09 • Start implementation of table.fs
– Implement the ST and JT functions store and retrieve – Test the JT functions with >let JT = [(“L1”, 2); (“L2”, 18); (“L3”, 23); (“L4”, 24)];
– Implement the instruction table functions fetchOpcode and fetchOperand
– Test the instruction table functions with > let instructions = [("get", "A"); ("get", "B"); ("put", "A"); ("put", "B"); ("halt","")];
Reading the WIC From a File • Here is the code
open System;
open System.IO;
open System.Collections.Generic;
open System.Windows.Forms;
let readFile =
let ofd = new OpenFileDialog() in
ofd.Title <- "Open a WIC File";
if ofd.ShowDialog() = DialogResult.OK then
Array.toList(File.ReadAllLines(ofd.FileName))
else
[]
• It should produce a result like val readFile : string list = ["get X"; "get Y"; "push X"; "push Y"; "pop X"; "pop Y";"put X"; "put Y"; "halt"]
The OpenFileDialog allows the user to browse the file system and select an input file.
Building the Instruction List and JT • Processing the list of instructions from the file
– An instruction such as “get A” must be added to the list of instruction tuples as (“get”, “A”)
– F# supports a string split function, <str name>.Split(‘ ‘) • This returns an array of Strings; items are indexed by using the
syntax <array name>.[0], <array name>.[1], and so forth • Arrays are OK in functional programming provided they are
immutable (that is, individual elements are not assigned new values)
– Handling a label instruction, such as “L1 label” at position 5
• Put the nop (no operation) instruction in the instruction table • Put the tuple (“L1”, 5) into the jump table
Representing the State of the Computation • Define the record structure type state = {
instructions: (string*string) list;
PC: int;
stack: int list;
ST: (string * int) list;
JT: (string * int) list;
}
• Creating a new machine state – Suppose the instruction only increments the PC value – The new state would be created as { state with PC = state.PC + 1 }
– All other fields would remain unchanged
If you defined named table types in table.fs, use those names here
Lab Activity ch05.10 • Start implementation of preprocess.fs
– After reading the WIC create the instruction table and the JT
– An initialize function should return the initial state of computation for the program
• Include the instruction table, it never changes • Include the PC, it is initially set to 0 • Include the runtime stack, it is initially empty • Include the ST, it is initially empty • Include the JT, it never changes
• Test your preprocess.fs on the WIC file with the gcd program; verify the instruction table and JT are correct
The main program • executeInstruction in interpreter.fs will contain the
following structure match (fetchOpcode state.PC state.instructions) with
|"get" ->
executeInstructions doGet state
|"put" ->
executeInstructions doPut state
|"push" ->
executeInstructions doPush state
First Group of Instructions • The first group of instructions will be put, get, push,
pop, halt, and nop • Here is the doPut instruction let doPut state =
// retrieve the operand from the list of instructions
let operand = fetchOperand state.PC state.instructions
// fetch the value from the symbol table
let value = retrieve operand state.ST
// print the operand and value to the console
printfn "%s = %d\n" operand value
// the only item changed in the new state is the PC
{ state with PC = state.PC+1 }
• The other instructions will have a similar format • Remember push is either a literal numeric value or
a variable whose value is in the ST
Lab Activity ch05.11 • Implement get, put, push, pop, halt, and nop • Place these functions in instructions.fs • Test your program with get A
get B
push A
push B
pop A
pop B
put A
put B
halt
This program will reverse the values of A and B
Arithmetic Instructions • Here is the logic for an add instruction let add state =
let num1 = state.stack.Head
let num2 = state.stack.Tail.Head
{ state with PC = state.PC + 1;
stack = num2 + num1 :: state.stack.Tail.Tail }
• The other arithmetic instructions will have a very similar format
• You can write a generic doArithmeticOp instruction let doArithmeticOp state op = …
• The call to this function will be something like |"add" ->
executeInstructions doArithmeticOp state (+)
Lab Activity ch05.12 • Implement the four arithmetic instructions; call
failwith on a divide by zero error • Test your program with get A
get B
push A
push B
add
pop Sum
put Sum
// expand to calculate
// and print sub, mult, divide
Logical Instructions • Here is the not instruction let doNot state =
let num1 = state.stack.Head
let value =
if num1 <> 0 then
0
else
1
{ state with PC = state.PC + 1;
stack = value :: state.stack.Tail }
• The and and or instructions will have a format similar to the arithmetic instructions
Lab Activity ch05.13 • Implement the three logical instructions • Test your program with get X1 or
get X2 push X1
get X3 not
get X4 push X2
push X1 or
push X2 push X4
not or
or and
push X3 pop Result
not pop Result
halt
Test Instructions • Here is the logic for a test less than instruction let testlt state =
let value = if state.stack.Head < 0 then 1 else 0
{state with PC = state.PC + 1; stack = value :: state.stack.Tail}
• The other test instructions will have a very similar format
• You can write a generic doTest instruction let doTest state op = …
• The call to this function will be something like |“tstlt" ->
executeInstructions doTest state (<)
Lab Activity ch05.14 • Implement the six test instructions in a single
doTest function • Test your program with get A
get B
push A
push B
sub
sub
tstlt
pop LTresult
put LTresult
// expand to test the other
// five comparisons
halt
Jump Instructions • Here is the unconitional jump instruction let doJump state =
// retrieve the operand from the list of instructions
let operand = fetchOperand state.PC state.instructions
// fetch the value from the jump table
let value = retrieve operand state.JT
// assign the PC this value
{ state with pc = value }
• The conditional jump instruction will be a bit more complex; remember it comes after a test instruction
Lab Activity ch05.15 • Test the jump instructions with get A
get B
push A
push B
sub
tstlt
jf L1
push B
pop MAX
j L2
L1 label
push A
pop MAX
L2 label
put MAX
halt
This program will find the largest of A and B
Lab Activity ch05.16
• Another test program get num
push 0
pop count
L1 label
push num
push 0
sub
tstgt
jf L2
push num
push 2
div
pop num
push count
push 1
add
pop count
j L1
L2 label
put count
halt
This program will print the number of binary digits in num
Testing the gcd program • First test the interactive version of the gcd program • Modify the program to remove the interactivity • Add the following timing functions let stopWatch = Stopwatch.StartNew()
...
stopWatch.Stop()
printfn "%f" stopWatch.Elapsed.TotalMilliseconds
• Time the gcd with M = 100000 and N = 1; continue increasing the value for M and collect timing data