The principal Big Ideas that we'll study in Miranda are polymorphic type systems and type inference. These are exhibited in a really crisp, clean form in Miranda. Later in the course we'll look at Java generics, and see how related ideas occur in object-oriented languages (complicated by the addition of inheritance).
Other important ideas in Miranda are:
Imperative languages have variables and assignment; they are in effect abstractions of the von Neumann machine. There is no referential transparency.
In mathematics, we can substitute equals for equals:
if x=y+z then you can substitute y+z for x
Informally, in a referentially transparent language you can substitute equals for equals, just as in mathematics. (You can't do that in an imperative language -- for example, in C or Java, just because you have a statement x=y+1 someplace, you can't safely substitute y+1 for x everywhere in your program.)
Haskell is a standard pure functional language, but it is much more complex than Miranda. ML has a functional subset.
Barriers to widespread use of functional languages:
mira
mira myfile.mThe standard extension for a Miranda source file is .m
Miranda will also generate a compiled version of the file with a .x extension -- if there is no .x version it creates one, and if the .m version is more recent it recompiles.
For a list of useful commands within Miranda type /help. These include the ability to edit the current program (or script, to use Miranda terminology). To do this, type /edit. The newly edited script is then loaded in. Other commands:
/man - online manual /quit (or control d)
There is a standard environment that comes pre-loaded with Miranda, with various useful functions. You can see what is there by looking at the "Standard Environment" option in the online manual. (This is a good thing to do not only to see what is available, but also to see some nice examples of Miranda programmming style.)
The default editor is vi, but you can change this to emacs or pico
or whatever you'd like by running this at the Miranda
prompt:
/editor emacs
(Note that you only have to do this once. Miranda will remember your editor choice across sessions.)
If you need to avoid having emacs show up in a separate x window for some reason, do this instead:
/editor emacs -nw
If you are using emacs on a .m file, the current version of emacs will think this is an Objective C file and open it in that mode. You can convince emacs to edit .m files in text mode by either creating a file named .emacs in your top-level directory on attu with the following line of code, or else adding this line to an existing .emacs file:
(setq auto-mode-alist (cons '("\\.m$" . text-mode) auto-mode-alist))
There is also an emacs package that defines a genuine Miranda mode -- if someone wants to try getting it working on attu let me know. (Incidentally, notice that emacs is using a Scheme dialect as its extension language!)
cp ~borning/miranda/lecture.m lecture.mAlso see ~borning/miranda/* on attu for other example programs (or scripts, to use the Miranda term).
Miranda 3+4 7 Miranda 8 : [1..10] [8,1,2,3,4,5,6,7,8,9,10] Miranda [1,2,3] ++ [8,9] [1,2,3,8,9] Miranda member [1,2,3] 8 FalseTo define a function:
double x = x+xFunction and other definitions must go in a file (script); Miranda distinguishes between definition mode (when reading from a file) and evaluation mode.
Variable names (including function names) must begin with a lower-case letter.
[2,4,7] [[1,2],[4,9]]
This would give a type error:
[2, 3, 's']
(Note the contrast with Scheme!)
strings -- shorthand for list of chars: "hi there"tuples: (False,[1,2,3],"str")
Tuples aren't the same as lists! Lists have indefinite length and all elements must be of the same type; tuples have fixed size but may have different types of elements.
functions are first class citizens
function application denoted by juxtaposition
neg 3 -3 member [1,4,6] 4 infix -- 3+4 (+) 3 4 arithmetic + - * / sin cos etc sum, product product [1,2,8]
|| recursive factorial || whitespace is significant - notice the use of layout in the example rec_factorial n = 1, if n=0 = n * rec_factorial (n-1), n>0 || alternate version using pattern matching: pattern_factorial 0 = 1 pattern_factorial (k+1) = (k+1) * pattern_factorial k || factorial using the built-in prod function factorial n = product [1..n] || mapping function, like map in Scheme || (Miranda also has a builtin function named map, so I gave it a new name here) my_map f [] = [] my_map f (a:x) = f a : my_map f x my_map factorial [1,5] my_map factorial [1..10] my_map factorial [1..]
Named after the logician Haskell Curry (not the spices).
First let's define a new function plus so that we don't get confused right off with infix notation:
plus x y = x+yNow we can apply plus to one argument to get the function that adds one to numbers:
addone = plus 1Here's the same function, without trying to avoid infix notation. (To use + as an ordinary function put parens around it.)
another_add1 = (+) 1Currying and higher-order functions:
twice f x = f (f x) g = twice add1 g 3 map factorial
hyp x y = sqrt sum where sum = x*x + y*y hyp2 x y = sqrt sum where sum = x2 + y2 where x2 = x*x y2= y*y
my_if True x y = x my_if False x y = y my_if (x>y) 3 4 my_if (x>y) 3 (1/0) ones = 1 : onesCompare with circular list in Scheme:
(define ones '(1)) (set-cdr! ones ones) ints n = n : ints (n+1) [1..] lets = "abc" ++ letsTwo prime number programs:
factors n = [k | k <- [1..n]; n mod k = 0] dullprimes = filter isprime [2..] where isprime p = (factors p = [1,p]) interestingprimes = sieve [2..] where sieve (p:x) = p : sieve [n | n <- x; n mod p > 0]Hamming numbers:
These are numbers whose only factors are powers of 2, 3, and 5. (See the Wikipedia article on Hamming numbers.)
my_merge (x:a) (y:b) = x : my_merge a (y:b) , if x<y = y : my_merge a b , if x=y = y : my_merge (x:a) b , otherwise ham = 1: my_merge ham2 (my_merge ham3 ham5) ham2 = map (*2) ham ham3 = map (*3) ham ham5 = map (*5) ham
twice f x = f (f x) double = (*) 2 twice double 4 map f [] = [] map f (a:x) = f a: map f x map ( (+) 1) [1..5]argument and result types of the function supplied to 'map' can be different:
map code "0123" [48,49,50,51]interesting uses:
member x a = or (map (=a) x) length x = sum (map (const 1) x) sum, product, and, or, concat, etc ... could define recursively, e.g. sum [] = 0 sum (a:x) = a+sum x foldr op r = f where f [] = r f (a:x) = op a(f x) sum = foldr (+) 0 product = foldr (*) 1 or = foldr (\/) False and = foldr (&) TrueThis does not hurt efficiency. Miranda's implementation uses combinator graphs -- the first time you run the function it rewrites that portion of the graph.
This is also the case for things like (1+3) and code '0' -- compare with the concept of 'constant folding' that you'll encounter in compilers.
Other examples:
my_filter f [] = [] my_filter f (a:x) = a : my_filter f x, f a = my_filter f x, otherwise reverse_args f x y = f y x const k x = k fix f = e where e = f e double x = 2*x const x y = x
3::
num
[1,2,3] ::
[num]
[[1,2,3]] ::
[[num]]
[]::
[*]
neg ::
num -> num
+ ::
num -> num -> num
(note that -> is right associative)
member::
[*] -> * -> bool
= ::
* -> * -> bool
run time error if = applied to functions
For append and member, universal polymorphism is just right. It isn't what we want for functions -- but Miranda fudges this with a runtime type check. Miranda avoids the complexities of overloading by having a single type num rather than integer and float types. (Haskell's type system includes type classes, which address both of these issues at the cost of added complexity.)
Universal polymorphism doesn't work well at all for object-oriented languages: there we want a type system that specifies that a variable x can hold any object that understands the + and * messages. (F-bounded polymorphism is a way to provide this.)
How does type checking work?
Variables are like the logic variables we'll encounter in CLP(R). The type checker uses unification (i.e. solving type equations -- you can think of it as a kind of two-way pattern matching).
Examples:
double x = x+x double::num->num map f [] = [] map f (a:x) = f a: map f x map::(*->**)->[*]->[**] map code "0123" [48,49,50,51] twice:: (*->*)->*->* map f [] = [] map f (a:x) = f a: map f x foldr::(*->**->**)->**->[*]->** foldr op r = f where f [] = r f (a:x) = op a(f x) sum = foldr (+) 0Here * and ** are type variables. (Aside: the * convention for naming type variables is not ideal when the types become complex - it would be better to have names formed using ordinary letters, perhaps with a special marker to distinguish type variables from ordinary variables. This is done in Haskell and ML.)
Example:
numtree ::= EmptyNumTree | NumNode num numtree numtree t1 = NumNode 7 EmptyNumTree (NumNode 4 EmptyNumTree EmptyNumTree) || polymorphic types: tree * ::= EmptyTree | Node * (tree *) (tree *) || define some sample trees t2 = Node 7 EmptyTree (Node 4 EmptyTree EmptyTree) t3 = Node 7 (Node 3 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree) || flattening a tree flatten :: tree * -> [*] flatten (Node n left right) = flatten left ++ [n] ++ flatten right flatten EmptyTree = [] || another way to flatten a tree (preorder traversal) pre (Node n left right) = [n] ++ pre left ++ pre right pre EmptyTree = [] || test for membership in a tree tmember :: * -> tree * -> bool tmember x EmptyTree = False tmember x (Node y left right) = True, x=y = tmember x left \/ tmember x right, otherwise || sum the elements in a tree of numbers tree_sum EmptyTree = 0 tree_sum (Node n left right) = n + tree_sum left + tree_sum right other_sum t = sum (flatten t) || note the types of tree_sum and other_sum: || tree num -> num || (thus these only work on one kind of tree - this is inferred automatically)The user-defined type mechanism encompasses enumeration types, union types, variant records:
weekday ::= Mon | Tues | Wed | Thurs | Fri intbool ::= MyInt num | MyBool bool || negate a number or apply not to a boolean flip (MyInt n) = MyInt (-n) flip (MyBool b) = MyBool (~b) intbool_list = [MyInt 5, MyInt (-10), MyBool True, MyBool False] flipped_list = map flip intbool_list || aliases string == [char] seacreatures :: [string] || this type declaration is equivalent to || seacreatures :: [[char]] seacreatures = ["clam", "squid"]
[ n*n*n | n <- [1..100] ]all cubes:
[ n*n*n | n <- [1..] ] perms [] = [[]] perms x = [ a:y | a <- x; y <- perms (x--[a]) ] || quicksort qsort [] = [] qsort (a:x) = qsort [ b | b <- x; b<=a ] ++ [a] ++ qsort [ b | b <- x; b>a ]Square root
sqt r = limit [x | x<-r, 0.5*(x + r/x).. ] limit::[*]->* limit (a:b:x) = a, if a=b = limit (b:x), otherwise limit other = error "incorrect use of limit" cp x y = [ (a,b) | a <- x; b <- y] Miranda cp [1..5] [1..4] [(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4), (3,1),(3,2),(3,3),(3,4),(4,1),(4,2),(4,3),(4,4),(5,1),(5,2),(5,3),(5,4)] cpdiag x y = [ (a,b) // a <- x; b <- y] rats = cpdiag [1..] [1..] [(1,1),(1,2),(2,1),(1,3),(2,2),(3,1),(1,4),(2,3),(3,2),(4,1), (1,5),(2,4),(3,3),(4,2),(5,1),(1,6),(2,5),(3,4),(4,3),(5,2), (6,1),(1,7),(2,6),(3,5),(4,4),(5,3),(6,2),(7,1),(1,8),(2,7), (3,6),(4,5),(5,4),(6,3),(7,2),(8,1),(1,9),(2,8),(3,7),(4,6),(5,5), (6,4),(7,3),(8,2),(9,1),(1,10),(2,9),(3,8),(4,7),(5,6),(6,5), ...force -- forces all parts of its argument to be evaluated
other functional languages that don't use combinators -- more conventional approach to lazy evaluation would be to pass a thunk
there, strictness analysis can win big in efficiency
abstype queue * with emptyq :: queue * isempty :: queue * -> bool head :: queue * -> * add :: * -> queue * -> queue * remove :: queue * -> queue * queue * == [*] emptyq = [] isempty [] = True isempty (a:x) = False head (a:x) = a add a q = q ++ [a] remove (a:x) = xOutside of the definition, the only way you can get at the queue is via its protocol. Example: building a queue
q1 = add 5 (add 3 emptyq) q2 = remove q1this won't work: q1 = [3,5]