Chapter 11 - Functional Programming, Part II: ML, Delayed
Download
Report
Transcript Chapter 11 - Functional Programming, Part II: ML, Delayed
Chapter 11 - Functional
Programming, Part II: ML,
Delayed Evaluation, and Haskell
Programming Languages:
Principles and Practice, 2nd Ed.
Kenneth C. Louden
© Kenneth C. Louden, 2003
1
Beyond Scheme
Static typing: Scheme does strong type checking
of values -- but only at execution time. Thus,
programs must be almost entirely debugged
during execution.
Delayed evaluation: Scheme's evaluation rule is
standard, which Scheme's built-in functions often
deviate from. To get other evaluation orders,
there is a significant programming overhead.
ML has strong static Hindley-Milner type
checking (Chapter 6).
Haskell is purely functional and has delayed
evaluation, which takes advantage of purity.
Haskell also has overloading, which Scheme &
ML do not.
Chapter 11 - Part II
K. Louden, Programming Languages
2
ML ("MetaLanguage")
Dates from 1978, with a new standard in
1997. Invented by Robin Milner (UK). Very
popular in Europe.
Standard translator is Standard ML of New
Jersey (Princeton & BellLabs). A popular
variant is CAML ("Categorical Abstract
Machine Language").
All definitions and expressions are type
checked prior to execution.
Otherwise very similar to Scheme, but
with a more C-like (or Ada-like) syntax.
Chapter 11 - Part II
K. Louden, Programming Languages
3
Sample ML Code
> fun fact n =
if n = 0 then 1 else n * fact(n-1);
val fact = fn: int -> int
> val Pi = 3.14159;
NOT multiplication
val Pi = 3.14159 : real
(see slide 13)
> 2 :: [3,4];
val it = [2,3,4] : int list
> (1,2,3.1);
val it = (1,2,3.1) : int * int * real
> fun append([],L) = L
| append(h::t,L) = h::append(t,L);
val append = fn : 'a list * 'a list -> 'a
list
Chapter 11 - Part II
K. Louden, Programming Languages
4
Notes about Sample ML Code
Each piece of code is followed by the interpreter
response as if entered directly (SML-NJ).
Arithmetic is infix, not prefix as in Scheme.
All expressions are statically typed (next slide).
There are two basic kinds of declarations, fun and
val (like the two kinds of defines in Scheme).
Lists are written using square brackets and
commas, with :: as the cons operator.
ML also has built-in tuples (Cartesian products),
written with commas and parentheses.
Functions can be defined using patterns (slide 11).
Chapter 11 - Part II
K. Louden, Programming Languages
5
Overview of static typing in ML
For more, see Chapter 6.
Historically, functional programmers resisted static
typing because they hated having to define the type
of every variable and function.
But static typing is clearly a good thing from a
software engineering perspective.
Hindley (a mathematician) showed already in 1969
how a translator could infer most types, but
computer scientists mostly ignored him.
Milner rediscovered Hindley's algorithm in 1978 and
applied it to ML ("MetaLanguage").
Chapter 11 - Part II
K. Louden, Programming Languages
6
Type checking in ML
Say we have a function definition in ML (slide 4):
fun fact n =
if n = 0 then 1 else n * fact(n-1);
ML has an easy time inferring its type:
val fact = fn : int -> int
It does so by assigning type variables 'a, 'b, etc. to
the identifiers and then uses the definition to infer
special information about those types: n = 0 shows
that n must have int type (comparison to an int).
The inferred types are as general as possible, so can
themselves contain type variables (i.e. are
polymorphic):
> fun id x = x;
val id = fn : 'a -> 'a
Chapter 11 - Part II
K. Louden, Programming Languages
7
Type checking in ML (2)
Type checking places some strong restrictions on
data, as noted in the following points.
Lists have to be all of the same type (like arrays):
> [1,2,3.1];
Error: operator and operand don't agree
[literal]
operator domain: int * int list
operand:
int * real list
in expression:
2 :: 3.1 :: nil
Lists and tuples cannot model recursive structures,
so user-defined types that can be recursive must be
introduced.
Chapter 11 - Part II
K. Louden, Programming Languages
8
Type checking in ML (3)
Hindley-Milner type checking usually requires no
type information, but sometimes it needs a type to
resolve an ambiguity:
> []@[]; (* @ is the append operation *)
stdIn:20.1-20.8 Warning: type vars not
generalized because of
value restriction are instantiated to
dummy types (X1,X2,...)
val it = [] : ?.X1 list
> ([]:int list)@[];
val it = [] : int list
Chapter 11 - Part II
K. Louden, Programming Languages
9
Type checking in ML (4)
ML also has a built-in prejudice towards integer
types in ambiguous arithmetic cases:
> fun square x = x * x;
val square = fn : int -> int
Thus, if we want a square function, say, on floating
point values, we must include a type:
> fun squaref (x:real) = x * x;
val squaref = fn : real -> real
Nevertheless, in most cases, Hindley-Milner type
checking removes the burden of specifying types.
Note also that ML's type system does not accept
overloading: if we want a square function for both
ints and reals we must use two different names.
Chapter 11 - Part II
K. Louden, Programming Languages
10
Equality types in ML
ML makes a strong distinction between ordinary
types and types for which the equality test (the =
operator) can be applied to values.
The = operator, unlike Scheme, is a general
equality test; however, it cannot be used on
functions or even real numbers:
> 2 = 2;
val it = true : bool
> map = map;
... Error: operator and operand don't
agree [equality type required]
> 2.0 = 2.0;
... Error: operator and operand don't
agree [equality type required]
Chapter 11 - Part II
K. Louden, Programming Languages
11
Equality types in ML (2)
Polymorphic type variables that must range over
equality types use two quotes instead of a single
quote:
> op =;
val it = fn : ''a * ''a -> bool
Anytime the = operator is used in code, the types
get restricted to the equality types:
> fun funny x = if x = x then x else x;
val funny = fn : ''a -> ''a
Make sure when writing code that you avoid using
= unless you really need it:
> fun empty L = if L = [] (* wrong! *)
then true else false;
val empty = fn : ''a list -> bool
Chapter 11 - Part II
K. Louden, Programming Languages
12
Pattern matching in ML
Hindley-Milner type checking relies on pattern
matching to infer types.
Pattern matching is also available to programmers,
so we can define functions on lists by using the
patterns [] (empty) and h::t (a list with head h
and tail t) -- the definition of append on slide 4 is
an example (equivalent to built-in @ function).
Patterns can also be used for numbers and tuples,
and the underscore _ can be used to match
anything anonymously:
fun fact 0 = 1 | fact n = n*fact(n-1);
fun first (x,_) = x;
Because of patterns, head and tail functions (hd &
tl in ML) are used much less than in Scheme.
Chapter 11 - Part II
K. Louden, Programming Languages
13
Pattern matching in ML (cont.)
The use of pattern matching and the vertical bar
in a function definition is just syntactic sugar for
a case expression:
fun fact 0 = 1 | fact n = n*fact(n-1);
is the same as
fun fact n =
case n of
0 => 1 |
_ => n * fact(n-1);
Non-exhaustive patterns result in a warning:
> fun hd (h::_) = h;
... Warning: match nonexhaustive
h :: _ => ...
val hd = fn : 'a list -> 'a
Chapter 11 - Part II
K. Louden, Programming Languages
14
Data structures in ML
ML has user-defined types, of two kinds.
Type synonyms -- just another name for an
existing type:
Asterisk means
> type Coords = real * real; "tuple" in a type
type Coords = real * real
> (2.1,3.2):Coords;
val it = (2.1,3.2) : Coords
Constructors - create
New types:
values of the type
datatype Direction
= North | East | South | West;
Either kind can be polymorphic:
type 'a Pair = 'a * 'a;
Chapter 11 - Part II
K. Louden, Programming Languages
15
Data structures in ML (cont.)
Constructor parameter
A polymorphic binary search tree:
(only one in ML)
datatype 'a BST = Nil
| Node of 'a 'a BST 'a BST;
A traversal function that uses pattern matching:
fun tree_to_list Nil = []
| tree_to_list(Node(d,l,r)) =
(tree_to_list l)@[d]@(tree_to_list r);
The type of this function is fn:'a BST -> 'a list
Here is a use:
val t =
Node("dog",Node("cat",Nil,Nil),Nil);
> tree_to_list t;
val it = ["cat","dog"] : string list
Chapter 11 - Part II
K. Louden, Programming Languages
16
Higher-order functions in ML
Like Scheme, ML has function-valued
expressions (lambdas in Scheme), but with a
somewhat different syntax:
> fn x => x * x;
val it = fn : int -> int
> (fn x => x * x) 4;
val it = 16 : int
> map (fn x => x * x) [2,3,4];
val it = [4,9,16] : int list
A fun declaration is really just a val declaration:
> val square = fn x => x * x;
val square = fn : int -> int
Chapter 11 - Part II
K. Louden, Programming Languages
17
Higher-order functions (cont.)
Unlike Scheme, top-level val declarations are not
automatically recursive in ML:
> val fact = fn n => if n = 0 then 1
else n * fact (n-1);
stdIn:20.45-20.49 Error: unbound
variable or constructor: fact
> val rec fact = fn n => if n = 0 then
1 else n * fact (n-1);
val fact = fn : int -> int
Another example of a higher-order function:
> fun twice f x = f (f x);
val twice = fn : ('a -> 'a) -> 'a -> 'a
> twice square 3;
val it = 81 : int
Chapter 11 - Part II
K. Louden, Programming Languages
18
Curried Functions
Notice the ML type of the twice function:
('a -> 'a) -> 'a -> 'a
-- a function taking a function from 'a to 'a as a
parameter and returning a function from 'a to 'a
as a result. Or: a function taking two parameters,
one of type 'a -> 'a and a second of type 'a,
and returning a value of type 'a as a result.
So we could also write
> twice square;
val it = fn : int -> int
Moral: the twice function can take either one or
two parameters; if you give it one, it returns a
function of the (missing) 2nd parameter
Chapter 11 - Part II
K. Louden, Programming Languages
19
Curried Functions (2)
Contrast the previous definition of twice with a
definition using a tuple:
> fun twice2 (f,x) = f (f x);
val twice2 = fn : ('a -> 'a) * 'a -> 'a
Now we must always supply both parameters at
every call:
> twice2 square;
Error: operator and operand don't agree
[tycon mismatch] ...
> twice2 (square,3);
val it = 81 : int
The twice function is Curried (named after
Haskell B. Curry), the twice2 function is not.
Chapter 11 - Part II
K. Louden, Programming Languages
20
Curried Functions (3)
Definition: A function taking multiple parameters is
Curried if it can be viewed as a (higher-order)
function of a single parameter.
Currying is good, since all functions can be viewed
as having just a single parameter, and higher-order
functions can be obtained automatically.
In ML, all function definitions not using tuples
explicitly are automatically Curried. (In Scheme the
opposite is true.)
However, in ML all infix functions are implicitly not
Curried:
> op +;
val it = fn : int * int -> int
Chapter 11 - Part II
K. Louden, Programming Languages
21
Variables in ML
Like Scheme, ML is not purely functional, and ML
functions need not be referentially transparent.
Unlike Scheme, variables in ML must be handled
differently from ordinary names; thus, nonfunctional constructs are more obvious.
A variable has type t ref, where t is the type of
the value stored in it.
ML uses := for assignment (like Ada), except that
the lhs must have type t ref and the rhs must
have type t.
To get the value out of a variable, you must
dereference it using the exclamation mark as a
(prefix) dereferencing operator.
Chapter 11 - Part II
K. Louden, Programming Languages
22
Variables in ML (cont.)
Here is sample code in ML, showing the use of a
variable, and a function inc that increments an
integer variable:
> val n = ref 2; (*must be initialized*)
val n = ref 2 : int ref
> fun inc x = x := !x + 1;
val inc = fn : int ref -> unit
> inc n;
Unit type is like void:
val it = () : unit
return value contains
> n;
val it = ref 3 : int ref no information.
> !n;
val it = 3 : int
Chapter 11 - Part II
K. Louden, Programming Languages
23
Sequencing and I/O in ML
Any group of expressions can be evaluated in
sequence by putting them inside a set of
parentheses and separating them by semicolons:
> (print "Hello ";print "World\n");
Hello World
val it = () : unit
Besides the print function there are many I/O
functions in the TextIO structure (like a Java
package):
> open TextIO; (* like Java import *)
... (* ML lists imports here *)
> fun echo () = let val s = inputLine
stdIn in output(TextIO.stdOut,s) end;
val echo = fn : unit -> unit
Chapter 11 - Part II
K. Louden, Programming Languages
24
Sequencing and I/O in ML (2)
We call the echo function by giving it the unit value:
> echo ();
now is the time
now is the time
val it = () : unit
String input can be converted to numbers using
various "fromString" functions, similar to "parse"
methods in Java:
> Real.fromString;
val it = fn : string -> real option
> Int.fromString "123";
val it = SOME 123 : int option
> Int.fromString "abc";
val it = NONE : int option
Chapter 11 - Part II
K. Louden, Programming Languages
25
Sequencing and I/O in ML (3)
Note in the fromString functions the use of an
option type to indicate that a conversion may fail:
> datatype 'a option = SOME of 'a | NONE;
Failure can also be represented in ML using
exceptions:
> exception SyntaxError;
exception SyntaxError;
> fun f x = if x <> 0 then x
else raise SyntaxError;
val f = fn : Int -> Int
> f 0 handle SyntaxError
=> (print "oops!\n";42);
oops!
val it = 42 : int
Chapter 11 - Part II
K. Louden, Programming Languages
26
Last example - a gcd test driver:
fun euclid () = (* from pages 501-502 of text *)
( output(stdOut, "Enter two integers:\n");
flushOut(stdOut);
let val u = Int.fromString(inputLine(stdIn));
val v = Int.fromString(inputLine(stdIn))
in
case (u,v) of
(SOME x, SOME y) =>
( output(stdOut,"The gcd of ");
output(stdOut,Int.toString(x));
output(stdOut," and ");
output(stdOut,Int.toString(y));
output(stdOut," is ");
output(stdOut,Int.toString(gcd(x,y)));
output(stdOut,"\n")
) |
_ => output(stdOut, "Bad input.\n")
end
)
Chapter 11 - Part II
K. Louden, Programming Languages
27
Delayed evaluation
ML and Scheme obey the standard "applicativeorder" evaluation rule (as do C and Java): all
parameters (arguments) are evaluated prior to the
execution of a call.
It is surprising how often we need a different rule:
(define (my-if a b c) (if a b c))
This definition can't work because both b and c
are evaluated before the call.
Scheme has manual facilities for delaying
evaluation of parameters:
(define (my-if a b c)
(if a (force b) (force c))
;; now the following code works:
(my-if #T (delay 1) (delay (/ 1 0)))
Chapter 11 - Part II
K. Louden, Programming Languages
28
Delay and force in Scheme
Delay creates a "promise" to evaluate an
expression, but doesn't evaluate it:
(delay (+ 3 4))
#<promise>
Force causes the "promise" to be fulfilled: the
expression is evaluated and the result returned
(but the promise is not turned into the value -- it
stays a promise):
> (define E (delay (+ 3 4)))
> (force E)
7
> E
#<promise>
Chapter 11 - Part II
K. Louden, Programming Languages
29
Uses of delayed evaluation
We can write "special forms" (functions not
obeying standard evaluation) directly in the
language.
Potentially unbounded data can be manipulated
without worrying about infinite loops:
(define (intlist m n)
(if (> m n) '()
(cons m (intlist (+ 1 m) n))))
But:
(define (ints-from m)
(cons m (delay (ints-from (+ m 1)))))
Chapter 11 - Part II
K. Louden, Programming Languages
30
Uses of delayed evaluation (cont.)
Now we can take as many integers from this
potentially infinite list as we want:
(define (take n L)
(if (= n 0) '()
(cons (car (force L))
(take (- n 1) (cdr (force L))))))
Then:
> (take 10 (delay (ints-from 1)))
(1 2 3 4 5 6 7 8 9 10)
Delayed lists are called streams, and they promote a
very effective programming technique called
generator-filter programming (see next slide).
Chapter 11 - Part II
K. Louden, Programming Languages
31
Generator-filter programming
A delayed filter function (see Chapter 11, Part I,
slide 30):
(define (d-filter p L)
Filters
(cond
((null? L) L)
Generator
((p (car (force L)))
(delay (cons (car (force L))
(d-filter p (cdr (force L))))))
(else (d-filter p (cdr (force L))))))
Now we can combine generators and filters:
> (take 20 (d-filter odd?
(delay (ints-from 1)))
(1 3 5 7 9 11 13 15 17 19 21 23 25 27 29
31 33 35 37 39)
Chapter 11 - Part II
K. Louden, Programming Languages
32
Memoization
Promises would be extremely inefficient, if they
evaluated their contained expressions every time
they are forced.
In fact, promises are memoized: the contained
expression is evaluated at most once, on the first
force; the value is then stored and used for all
subsequent forces, as shown in Scheme by
using take with the following rewrite of the
ints-from function of slide 24 (it only prints
each number once):
(define (ints-from n)
(display "computing ")
(display n)(newline)
(cons n (delay (ints-from (+ n 1)))))
Chapter 11 - Part II
K. Louden, Programming Languages
33
Lazy Evaluation
Built-in delayed evaluation with
memoization is called lazy evaluation
It works well only with pure functional
programs, since the order of evaluation is
unpredictable (so all functions must be
referentially transparent).
Chapter 11 - Part II
K. Louden, Programming Languages
34
Properties of lazy evaluation
All arguments to user-defined functions are delayed.
All bindings of local names in let and letrec
expressions are delayed.
All arguments to constructor functions (such as
cons) are delayed.
All arguments to other predefined functions, such
as the arithmetic functions "+," "*," and so on are
forced.
All function-valued arguments are forced.
All conditions in selection functions such as if and
cond are forced.
Chapter 11 - Part II
K. Louden, Programming Languages
35
Haskell
A fully-Curried lazy purely functional language
with Hindley-Milner static typing. (Fully-Curried
means all functions, including built-in arithmetic,
are implicitly Curried.)
Has many other features that make it perhaps the
most advanced functional language available
today.
Includes overloading and a built-in mechanism
for generator-filter programming (called list
comprensions -- see slide 36).
Chapter 11 - Part II
K. Louden, Programming Languages
36
Sample Haskell code:
fact n =
if n == 0 then 1 else n * fact (n-1)
square x = x * x
pi1 = 3.14159
gcd1 u v =
if v == 0 then u else gcd1 v (mod u v)
squarelist lis =
if (null lis) then lis
else square (head lis):
squarelist (tail lis)
Chapter 11 - Part II
K. Louden, Programming Languages
37
Haskell properties
Parentheses are only necessary for grouping.
Semicolons are usually unnecessary (see slide 34).
Definitions have no special syntax, so they must be
loaded from a file—they cannot be entered directly
into an interpreter.
Operators are infix.
Lists are written using square brackets and commas
(e.g. [1,2,3]), with ++ as the list append operation,
and : (colon) as the list constructor operation.
All value and function names must start with a
lowercase letter, and all types are uppercase: Int.
Names cannot be redefined, hence the use of pi1 &
gcd1 in the previous slide (pi & gcd are predefined).
Chapter 11 - Part II
K. Louden, Programming Languages
38
Haskell properties (2)
Haskell is purely functional, so there are no
variables or assignments, and I/O and sequencing
cannot be done except using special tools, called
monads (which we do not study here).
Of course, there are still local definitions:
let x = 2; y = 3 in x + y
or: let x = 2
y = 3 in x + y
Note indentation in the previous code to get rid of
the semicolon: Haskell uses a two-dimensional
Layout Rule to remove extra syntax. Leading white
space matters!
Chapter 11 - Part II
K. Louden, Programming Languages
39
Haskell properties (3)
Note lambda
syntax
All local definitions are recursive:
f x =
let z = x-1
g = \y->if y==0 then z else g(y-1)
in g x + 2
Comment
All expressions are delayed in Haskell:
ones = 1:ones -- can also write [1,1..]
ints_from n = n : ints_from (n+1)
ints = ints_from 1 -- also: [1..]
Infix operators can be made prefix by putting
parentheses around them:
double lis = map ((*) 2) lis
Chapter 11 - Part II
K. Louden, Programming Languages
40
Haskell properties (4)
Hindley-Milner type checking is performed on all
definitions and expressions. Type variables use the
names a, b, c, etc. (in contrast to ML, which uses
single quotes before the name).
Types are not automatically displayed as in ML, but
can be viewed by using the :t command in the
interpreter.
Small sample interpreter session:
> [1]++[2]
[1,2]
> :t (++)
(++) :: [a] -> [a] -> [a]
> [1]++['a']
ERROR - ...
Chapter 11 - Part II
K. Louden, Programming Languages
41
Patterns in Haskell
Patterns can be used in function definitions as in
ML, except that no vertical bar needs to separate
different patterns in a function definition (all
patterns must, however, be written together).
Typical patterns are 0 for zero, [] for the empty list,
and (x:xs) for a nonempty list.
fact and squarelist from slide 31 using patterns:
fact 0 = 1
fact n = n * fact (n-1)
squarelist [] = []
squarelist (x:xs) =
(square x) : squarelist xs
Of course, it would be better to use map:
squarelist lis = map square lis
Chapter 11 - Part II
K. Louden, Programming Languages
42
Patterns in Haskell (cont.)
Because of patterns, it is unusual in Haskell to use
head, tail, and null.
The anonymous wildcard pattern (matches
anything) is the underscore in Haskell as in ML.
Overlapping patterns are ok (see fact on previous
slide). Non-exhaustive patterns can also be used,
and do not generate a warning as in ML:
head1 (h:_) = h -- no warning message!
(Of course, the call head [] will still result in an
error.)
Patterns are syntactic sugar for case expressions:
fact n = case n of
0 -> 1
_ -> n * fact (n-1)
Chapter 11 - Part II
K. Louden, Programming Languages
43
Haskell List Comprehensions
Generators and filters can be expressed together in
Haskell in a quasi-list notation called a list
comprehension:
odds = [n | n <- ints, mod n 2 /= 0]
-- can also write [1,3..]
Multiple generators and filters can be combined in a
single list comprehension:
mystery =
[n+m|n <-ints, m <-ints, mod n m /= 0]
List comprehensions allow many snappy programs
to be written:
sieve (h:t)
= h:sieve [n|n <- t, mod n h /= 0]
primes = sieve [2..]
Chapter 11 - Part II
K. Louden, Programming Languages
44
Haskell Data Structures
So far we have only seen lists in Haskell,
although we know that static typing does not
allow lists to imitate structs or classes as in
Scheme.
Haskell has built-in tuple types (like ML), which
are Cartesian products (like structs but with no
field names):
intWithRoot:: Int -> (Int,Double)
intWithRoot x = (x , sqrt (fromInt x))
Use patterns to get the components out:
rootOf x = let (_,r)=intWithRoot x in r
Tuple types are written the same way values are,
(not like ML, which uses asterisks).
Chapter 11 - Part II
K. Louden, Programming Languages
45
Haskell Data Structures (2)
Lists and tuples do not go far enough, since unions
cannot be expressed.
User-defined types can be introduced using data
definitions:
data Direction = North|East|South|West
Very similar to ML datatype definition: the name on
the left of a data definition is the type name; the
names on the right are constructors representing
values.
Haskell can have polymorphic types and
constructors with more than one parameter (and
type variables are written after the type name, not
before as in ML):
data Either1 a b = Left1 a | Right1 b
Chapter 11 - Part II
K. Louden, Programming Languages
46
Haskell Data Structures (3)
Note how the previous definition expresses a
tagged union of two polymorphic types.
Binary search tree example (recursive type):
data BST a = Nil | Node a (BST a) (BST a)
simpleBST = Node "horse" Nil Nil -- value
Constructors can be used as patterns:
tree_to_list Nil = []
tree_to_list (Node val left right) =
(tree_to_list left) ++ [val]
++ (tree_to_list right)
Type synonyms can also be defined:
type IntDouble = (Int,Double)
Note: all type and constructor names must be
uppercase.
Chapter 11 - Part II
K. Louden, Programming Languages
47
Overloading in Haskell
Many functions in Haskell are overloaded, in that
they can take values from a (finite) number of
different types. An easy example is the square
function, defined by square x = x * x.
The type of square in Haskell is:
square :: Num a => a -> a
This says basically that square is defined for any
Num a type (such types all have a * function).
The type Num a => a is called a qualified type,
and Num is called a type class.
Type classes are a bit like Java interfaces: they
require that a certain function be defined, and
each associated type must then implement it.
Chapter 11 - Part II
K. Louden, Programming Languages
48
Overloading in Haskell (2)
Here is an example of how to define a Sizeable
type class that provides a measure of the size of
a piece of data:
class Sizeable a where
size:: a -> Int
Now any type that you want to implement
Sizeable must be declared an instance of the
Sizeable class: (i.e., belongs to that class):
instance Sizeable [a] where
size = length
instance Sizeable (BST a) where
size Nil = 0
size (Node d l r) =
(size l)+(size r) + 1
Chapter 11 - Part II
K. Louden, Programming Languages
49
Overloading in Haskell (3)
Now any use of the size function automatically
adds the Sizeable qualification to a type:
trivial x = size x == 0
This function has type:
Sizeable a => a -> Bool
Type classes usually require multiple functions:
class Num a where
(+), (-), (*) :: a -> a -> a
negate
:: a -> a
abs
:: a -> a ...etc.
Built-in "hidden"
instance Num Int where
definitions
(+)
= primPlusInt
(-)
= primMinusInt
negate
= primNegInt ...etc.
Chapter 11 - Part II
K. Louden, Programming Languages
50
Overloading in Haskell (4)
Type classes may need to "inherit" functions
from other type classes (similar to interface
inheritance in Java):
class Eq a where
(==), (/=) :: a -> a -> Bool
x == y
= not (x/=y)
Note default
definitions
x /= y
= not (x==y)
class (Eq a) => Ord a where
(<),(<=),(>=),(>) :: a -> a -> Bool
max, min
:: a -> a -> a
instance Eq Int where …
instance Ord Int where ...
Chapter 11 - Part II
K. Louden, Programming Languages
51
Numeric Type Class Hierarchy
Eq
(==)
Ord
(<)
Num
(+,-,*)
Real
Integral
(div, mod)
Show
(show )
Fractional
(/)
RealFrac
(round, trunc)
Floating
(sin, cos)
RealFloat
Chapter 11 - Part II
K. Louden, Programming Languages
52
Overloading in Haskell (5)
The Show class allows a data type to be displayed
as a String (e.g. by the interpreter):
class Show a where
show
:: a -> String
So many data types need to be made instances of
Show and Eq that Haskell can do it automatically:
data BST a =
Nil | Node a (BST a) (BST a)
deriving (Show,Eq)
Overloading presents challenges for Hindley-Milner
type checking that result in some surprises:
squarelist = map square (compare to slide 36)
gives squarelist the type
[Integer] -> [Integer] (no overloading!)
Chapter 11 - Part II
K. Louden, Programming Languages
53