An introduction to Functional Programming in F# -- Part 5: Objects and Exceptions
This session marks a pause for breath between the introductory parts where we concentrated on doing interesting work with mostly simple immutable lists and with algebraic datatypes, and the last part which will be all heavy lifting, to look at other ways of representing compound data, and how the functional world interacts with the more familiar parts of the .net framework. Also, this week, no exercises either.
I'm keeping this part in mainly for completeness' sake, as a transcription of the original seminar material from '08; rather than for any great insights it offers. In particular, unlike the previous sessions there won't be any of the two steps to comprehend -- first, how the program works, and then with increasing familiarity, why you wouldn't actually implement things in quite that way in a real program. It'll be more like a shopping list instead.
Topics covered in the series
- Introduction to Functional Programming via circuits
- Lexing and Parsing
- Interpreters
- Simple ‘Functional’ Distributed/Concurrent Programming
- Non-Functional bits - Assignment, Exceptions, Objects, Libraries, etc. (this part)
- Advanced stuff - Continuations, Monads, Generative Programming (Language Based programming), etc.
Assignment
Actually we covered assignment and mutability last time.
Tuples
We used tuples -- collections of a fixed number of items, defined by the types of each item, way back in the first session. Recall
let Half_Adder x y = (XOR x y, AND x y);; |
The value returned by this function is a 2-tuple of the Bits
type -- written Bits * Bits
; but in general the types in a tuple will be heterogeneous.
Tuples are usually best employed as ad hoc datastructures; while there are standard functions
let fst (a,b) = a;; | |
let snd (a,b) = b;; |
for extracting members of a 2-tuple, getting at parts of longer tuples will involve destructuring pattern matching.
Records
A record is more like a class -- or, really, a 'C'-style struct -- in having a series of named fields. We have actually used records in their simplest form, in some of our previous examples
type SEXPR = SYMBOL of string | NUMBER of int | NIL | CONS of (SEXPR * SEXPR) | COMPUTATION of COMP | |
and COMP = { func : SEXPR -> SEXPR } ;; |
because functions are just values, we can put them into a record just as well as other types. We can be more object like if we mix scalar and function values in a record:
type Point = { x: int; y: int; move: Point -> Point } | |
let rec makePoint vx vy = | |
let doMove p = makePoint (vx + p.x) (vy + p.y) | |
{ x = vx; y = vy; move = doMove };; |
which defines a 2D point type with a displacement-vector behaviour.
Records themselves are immutable, so we can only create new records based off old ones e.g.
let p = (makePoint 1 2).move(makePoint 2 3);; |
There is a copy but replace one or more fields syntax
{p with x = 6; y = 0 };; |
but this of course violates the contract that the move function displaces its target by the x,y value of the point. However, types can have methods
type Point = { mutable x: int; mutable y: int } | |
with member p.move q = p.x <- p.x + q.x; | |
p.y <- p.y + q.y | |
let makePoint x y = { x = x; y = y } |
and with the move
member working on the current rather than creation time value of the coordinates, we have a class-like structure, albeit with public fields. And most of the time this is sufficient, if the type itself is not exposed.
Objects and interfaces
When interfacing with .net libraries, we need to define actual objects or interfaces. Our previous point behaviour can be expressed as an interface
type IPoint | |
= interface | |
abstract x: int; | |
abstract y: int; | |
abstract move: IPoint -> unit | |
end |
which we can implement as
type Point | |
= class | |
val mutable vx: int; | |
val mutable vy: int; | |
new(x,y) = { vx = x; vy = y } | |
interface IPoint | |
with member p.move q = p.vx <- p.vx + q.x; | |
p.vy <- p.vy + q.y | |
member p.x = p.vx | |
member p.y = p.vy | |
end | |
let makePoint x y = new Point(x,y) :> IPoint;; |
Note that F# syntax requires us to explicitly implement the interface; as a consequence, we also need to cast our concrete implementation explicitly to the interface type when we wish to refer to it as such.
There is an alternative syntax which removes the explicit new
member as a constructor, and instead makes the class body act as a constructor function:
type Point(x,y) | |
= class | |
let mutable vx = x | |
let mutable vy = y | |
interface IPoint | |
with member p.move q = vx <- vx + q.x; | |
vy <- vy + q.y | |
member p.x = vx | |
member p.y = vy | |
end |
The two are similar, but not quite the same; for this second example vx and vy are not (as the result of pasting the code into the interactive prompt shows) instance variables, as they were before, but are in locals in the constructor function.
Alternatively, we can define an abstract base class, rather than an interface. This requires an attribute annotation, rather than a keyword:
[<AbstractClass>] | |
type AbstractPoint | |
= class | |
val mutable vx: int; | |
val mutable vy: int; | |
new(x,y) = { vx = x; vy = y } | |
member p.x with get() = p.vx and set z = p.vx <- z | |
member p.y with get() = p.vy and set z = p.vy <- z | |
abstract move: AbstractPoint -> unit | |
end | |
type Point' | |
= class | |
inherit AbstractPoint | |
new(x,y) = { inherit AbstractPoint(x,y) } | |
override p.move q = p.x <- p.x + q.x; | |
p.y <- p.y + q.y | |
end | |
let p = Point'(2,3) | |
let q = Point'(1,2) | |
p.move(q) | |
p.x | |
p.y;; |
Note that this time we don't have to cast anything to the base class in order to invoke the move
method. A Point'
is an AbstractPoint
.
Exceptions
These have the sort of behaviour that we are familiar with
- Exceptions do not affect the types of functions that throw them.
- Exceptions “propagate up the call stack” until a handler is found (or to top level).
There are two built-in exception mechanisms
// Raise an ArgumentException: | |
invalid_arg "list must not be empty" | |
// Raise a FailureException: | |
failwith "internal error" |
or we can just raise a standard exception type
raise (InvalidOperationException("help!")) |
New exception types can be declared by subclassing, or by ML syntax
exception Special of int | |
raise (Special 99) |
Exceptions are handled much like they are in C#/C++/Java...
exception EmptyList of string | |
let rec folds g x | |
= match x | |
with [] -> raise (EmptyList "folds") | |
| [x] -> x | |
| h::t -> g h (folds g t) | |
try folds (fun x y -> x + y) [] | |
with EmptyList m -> stdout.WriteLine(m); | |
0 |
where the with
construct actually allows pattern matching over the exception (so can be the equivalent of multiple catch
clauses)
There is a related construct; a finally
clause can also be used with try
as in C#, to add some code which is always executed:
let resource = getResource() | |
in try folds (useResource resource) [] | |
with EmptyList m -> stdout.WriteLine(m); | |
rethrow() | |
finally releaseResource resource |
though there is no portmanteau try ... catch ... finally
as there is in C#.
Note that because we are in the .net framework here, exceptions are expensive operations, unlike in OCAML, where they are sufficiently cheap as to be a standard control flow construct.
No comments :
Post a Comment