330 likes | 343 Views
Learn about staged computation, meta-programming in MetaML, and differences between Haskell and ML in this advanced functional programming lecture. Explore type constructors, data definitions, function definitions, primitives, and more.
E N D
Advanced Functional Programming • Tim Sheard • Oregon Graduate Institute of Science & Technology • Lecture: Staged computation • Difference between Haskell and ML • Intro to MetaML • Using staging to control evaluation order
type variables and type application 'a Tree type constructors int, string, bool tuples (int * string * int) type variables and type application Tree a type constructors Int, String, Bool tuples (Int, String, Bool) Differences in Types
(* Data definitions *) datatype 'a Tree = Tip of 'a | Fork of ('a Tree)*('a Tree) 'a = type variable constructors are not capitalized -- data definitions data Tree a = Tip a | Fork (Tree a) (Tree a) Differences comments Type constructors are post fix note use of "of" constructor functions are always uncurried (use tuples)
(* function definitions *) fun fact n = if n=0 then 1 else n * (fact (n-1)); fun depth (Tip a) = 0 | depth (Fork(x,y)) = 1 + max (depth x, depth y); -- function definitions fact n = if n==0 then 1 else n * (fact (n-1)) depth (Tip a) = 0 depth (Fork x y) = 1 + max (depth x) (depth y) difference 2 note use of "fun" equality is double = use "bar" (|) to separate clauses primitives more likely to be curried in Haskell
Data List a = [] | (:) a (list a) map f [] = [] map f (x:xs)=(f x):(map f xs) pi = 3.14159 \ x -> x + 1 Datatype ‘a list = [] | (op ::) of ‘a * (‘a list) fun map f [] = … | map f (x::xs) = val pi = 3.14159 fn x => x + 1 Differences between Haskell & ML
- op +; val it = fn:int * int -> int - op *; val it = fn:int * int -> int - op -; val it = fn:int * int -> int - op @; val it = fn:'a list * 'a list -> 'a list - length; val it = fn:'a list -> int - op ::; val it = fn:'a * 'a list -> 'a list - hd; val it = fn:'a list -> 'a - tl; val it = fn:'a list -> 'a list Main> :t (+) (+) :: Num a => a -> a -> a Main> :t (*) (*) :: Num a => a -> a -> a Main> :t (-) (-) :: Num a => a -> a -> a Main> :t (++) (++) :: [a] -> [a] -> [a] Main> :t length length :: [a] -> Int Main> :t (:) (:) :: a -> [a] -> [a] Main> :t head head :: [a] -> a Main> :t tail tail :: [a] -> [a] Some Primitives
(* variable definition *) val tree1 = Fork(Fork(Tip 1,Tip 2), Tip 3); -- variable definition tree1 = Fork (Fork (Tip 1) (Tip 2)) (Tip 3) Differences 3 use val to declare variables
fn x => x + 1 let fun f x = x + 1 in f 7 end has type in ML is : datatype T = ... datatype ('a,'b) T = ... = (in MetaML only) 'a ('b T) \ x -> x + 1 let f x = x + 1 in f 7 has type in Haskell is :: data T = ... data T b a = ... functions
case x of [] => 5 | (x::xs) => len xs + m3 case x of [] -> 5 (x:xs) -> len xs + m3 case expressions
(* booleans *) - true; val it = true : bool - false; val it = false : bool (* mutual recursion *) fun even 0 = true | even n = odd (n-1) and odd 0 = false | odd n = even (n-1); -- booleans Main> :t True True :: Bool Main> :t False False :: Bool -- mutual recursion even 0 = True even n = odd (n-1) odd 0 = False odd n = even (n-1) Differences 4 No capitals all functions are implicitly mutually recursive use of "and" must separate mutually recursive functions
Controlling Evaluation Order • We want more than just the correct result! • We want to control other resources such as time and space without resorting to tricks or unnatural programming styles. • Mechanism - Control Evaluation Order
Traditional Approaches • Fixed evaluation order with language extensions • Lazy - Outermost • add strictness annotations • Strict - Innermost • add annotations like force and delay • Encode laziness using lambda in a strict setting • datatype 'a lazylist = • lazyNil • | lazyCons of 'a * (unit -> 'a lazylist); • fun count n = lazyCons(n, fn () => count (n+1))
Limitations • None of these approaches allow computation under a lambda! This is sometimes just what is needed. For example: • fun power n = • (fn x => if n=0 • then 1 • else x * (power (n-1) x)) • power 2;
Example reduction • (power 2) • unfold the definition • (fn x => if 2=0 then 1 else x * (power (2-1) x)) • perform the if, under the lambda • (fn x => x * (power (2-1) x)) • unfold power again • (fn x => x * ((fn x => if 1=0 • then 1 • else x * (power (1-1) x)) • x)) • use the beta rule to apply the explicit lambda to x
Example (cont.) • (fn x => x * (if 1=0 then 1 else x * (power (1-1) x))) • perform the if • (fn x => x * (x * (power (1-1) x))) • unfold power again • (fn x => x * (x * ((fn x => if 0=0 • then 1 • else x * (power (0-1) x))) • x)) • use the beta rule to apply the explicit lambda to x • (fn x => x * (x * (if 0=0 then 1 • else x * (power (0-1) x)))) • perform the if • (fn x => x * (x * 1))
Solution - Use richer annotations • Brackets: < e > • no reductions allowed in e • delay computation • if e:t then <e> : <t> (pronounced code of t) • Escape: ~ e • relax the no reduction rule of brackets above • Escape may only occur inside Brackets • splice code together to build larger code • Run: run e • remove outermost brackets • force computations which have been delayed
Calculus • A calculus describes equivalences between program fragments. • The rules of a calculus can be applied in any order. • An implementation applies the rules in some fixed order. • Traditional rules • beta – (fn x => e) v e[v/x] • if – if true then x else y x • – if false then x else y y • delta – 5 + 2 7
Rules for code • Introduction rule for code • < e > • 1st elimination rule for code (escape-bracket elim) • < … ~<e> … > ---> < … e … > • ~<e> must appear inside enclosing brackets • e must be escape free • <e> must be at level 0 • 2nd elimination rule for code (run-bracket elim) • run <e> ---> e • providedehas no escapes • the whole expression is at level 0
Power example revisited • (* power : int -> <int> -> <int> *) • fun power n = • fn x => if n=0 • then <1> • else < ~x * ~(power (n-1) x) > • (* ans : < int -> int > *) • val ans = < fn z => ~(power 2 <z>) >;
<fn z => ~ (power 2 <z>) > <fn z => ~(if 2=0 then <1> else < ~<z> * ~(power (2-1) <z>) >)> <fn z => ~< ~<z> * ~(power (2-1) <z>) >>) <fn z => ~< z * ~(power (2-1) <z>) >>) <fn z => ~< z * ~(if 1=0 then <1> else < ~<z> * ~(power (1-1) <z>) >) >>)
<fn z => ~< z * ~< ~<z> * ~(power (1-1) <z>) >>> <fn z => ~< z * ~< z * ~(power (1-1) <z>) >>> <fn z => ~< z * ~< z * ~(power 0 <z>) >>> <fn z => ~< z * ~< z * ~<1> >>> <fn z => ~< z * ~< z * 1 >>> <fn z => ~< z * z * 1 >> <fn z => z * z * 1>
MetaML: Meta-programming • Meta-Programming: Programs that write programs • What Infrastructure is possible in a language designed to help support the algorithmic construction of other programs? • Advantages of meta-programs • capture knowledge • efficient solutions • design ideas can be communicated and shared
Types of meta-programs • Static generators • Generate code which is then “written to disk” and processed by normal compilers etc. • Two kinds • Homogeneous systems • object language = meta-language • Heterogeneous systems • object-language different from meta-language • Run-time generators • Staged Programs • Programs that generate other programs, and then execute the programs they generated • MetaML is a staged programming language which does run-time code generation
MetaML & the Mustang Project • Theory • Semantics of staged programs • What does it mean to have a staged program? • How can we tell if a staged program computes the same result as its unstaged counterpart? • Calculus of programs - Rules for reasoning • Staged type systems • A static type system keeps bad programs from executing by throwing away bad programs by refusing to compile them. • A staged program admits more bad things. • E.g. using a variable before it is defined, or executing a program before its complete, for example. • Staged languages need richer type systems to throw away these bad programs. • Automatic staging • partial evaluation
MetaML & the Mustang Project • Tools • Staged Languages • MetaML • reflective: obj-lang = meta-lang = ML • multi-stage • Heterogeneous (meta-lang=ML, obj-lang varies, 2 stage)
Building pieces of code • -| <23>; • val it = <23> : <int> • -| <length [1,2,3]>; • val it = <%length [1,2,3]> :<int> • -| <23 + 5>; • val it = <23 %+ 5> : <int> • -| <fn x => x + 5>; • val it = <(fn a => a %+ 5)> : <int -> int> • -| <fn x => <x + 1>>; • val it = <(fn a => <a %+ 1>)> : <int -> <int>>
Let’s try it ! Run MetaML
Composing pieces of code • -| val x = <2>; • val x = <2> : <int> • -| val y = <44 + ~x>; • val y = <44 %+ 2> : <int> • -| fun pair x = <( ~x, ~x )>; • val pair = Fn : ['b].<'b > -> <('b * 'b )> • -| pair <11>; • val it = <(11,11)> : <(int * int)>
Executing constructed code • -| val x = <34 + 2>; • val x = <34 %+ 2> : ['a].<int> • -| run x; • val it = 36 : int • -| fun id x = x; • val id = Fn : ['a].'a -> 'a • -| val q = <1 + ~(id <2+3>) >; • val q = <1 %+ 2 %+ 3> : <int> • -| run q; • val it = 6 : int
Multi-stage code • -| val z = <fn n => <n + 1>>; • val z = <(fn a => <a %+ 1>)> : <int -> <int>> • -| val f = run z; • val f = Fn : int -> <int> • -| f 12; • val it = <%n %+ 1> : <int> • -| run it; • val it = 13 : int
The lift operator • -| <4+1>; • val it = <4 %+ 1> : <int> • -| lift (4+1); • val it = <5> : <int> • -| val z = <fn n => < ~(lift n) + 1>>; • val z = <(fn a => <~(lift a) %+ 1>)> :<int -> <int>> • -| run z; • val it = Fn : int -> <int> • -| it 5; • val it = <5 %+ 1>: <int> • -| run it; • val it = 6 : int
Lift v.s. lexical capture • Lift cannot be used on functions • -| lift id; • Error: The term: id • Non Qualified type, not liftable: ? -> ? • Lift makes code readable • -| fun f x = <(x, ~(lift x))>; • val f = Fn : ['b^].'b -> <('b * 'b )> • -| f 3; • val it = <(%x,3)> : <(int * int)> • Lexical capture is more efficient • -| lift [1+3,4,8-4]; • val it = <4 :: (4 :: (4 :: [])))> : <int list >