480 likes | 616 Views
Introduction to Pizza : an Extension to Java. Chegn-Chia chen. The Pizza Language. An extension to Java with three new features: Generics (aka Parametric polymorphism) Function pointers (aka First-class functions) Class cases and pattern matching (aka Algebraic types). outlines.
E N D
Introduction to Pizza : an Extension to Java Chegn-Chia chen
The Pizza Language • An extension to Java with three new features: • Generics (aka Parametric polymorphism) • Function pointers (aka First-class functions) • Class cases and pattern matching (aka Algebraic types)
outlines • Parametric Polymorphism (Generics or Templates) including • Bounded Polymorphism • F-bounded Polymorphism • First-class functions (Function pointers or Function variables) • Anonymous functions • Algebraic types including • Pattern matching • Enumeration types (enum) • Type casts • Tail call recursion • Pizza's API classes
Parametric Polymorphism • allow us to write classes that operate on data without specifying the data's type. • Ex: class Store<A> { A data; Store(A data) { this.data = data; } void set(A data) { this.data = data; } A get() { return data; } } • is a general classes with type parameter <A>
Instantiation of parametric classes Store<String> s1 = new Store(“This is a string!"); Store<int> s2 = new Store(23 +14); s2.set(19); int i = s2.get(); String s = s1.get(); • Notes: • when allocating an Object of a parametric type, we don't need to pass the instance type. (i.e., not new Store<int>(23 + 14) ) • The Pizza compiler infers this type from the constructor arguments
What we would use with Java • Strategy 1: replace all type parameter with Object. class Store /*<A>*/ { Object /*A*/ data; Store(Object /*A*/ data) { this.data = data; } void set((Object /*A*/ data) { this.data = data; } Object /*A*/ get() { return data; } } … Store s1 = new Store(“This is a string!”);s1.set(“new string”); String rlt = (String) s1.get(); • Problems: • 1. can not deal with int case. • 2. additional type cast required.
Extend parametric Class class StoreString extends Store<String> { Store(String something) { super(something); } } • special class storing only String object. class Store2<A> extends Store<A> { Store2(A data) { super(data); } void print() { System.out.println(""+data); } } • This gives us a parameterised class extending the other parameterised class.
The Pair Parametric class • public class Pair<A, B> { public A fst; public B snd; public Pair(A fst, B snd) { this.fst = fst; this.snd = snd; } } • This class becomes very handy, if you need a method to return two values: • Pair<String, int> getValues() { return new Pair(aString, anInt); } • included in the standard Pizza classes class pizza.Pair
Parametric Interfaces • All the things we did to classes can also be done to interfaces. • Ex: sets of elements of a parameterised type A: interface Set<A> { boolean contains(A x); Set<A> include(A x); // return this U {x}. } • The method include(A) shows us the sets defined by this interface are persistent: • Every time we include an element we get a new set (it doesn't change the old one).
Implement Set<A> using LinkedList • abstract class LinkedList<A> implements Set<A> { public abstract boolean contains(A x); public LinkedList<A>insert(A x) { if (contains(x)) return this; else return new NonEmptyList(x, this); } public Set<A>include(A x) { return insert(x); } } • class EmptyList<A> extends LinkedList<A> { public boolean contains(A x) { return false; } }
class NonEmptyList<A> extends LinkedList<A> { private A head; private LinkedList<A> rest; public NonEmptyList(A elem, LinkedList<A> rest) { this.head = elem; this.rest = rest; } public boolean contains(A x) { return ((Object)head).equals((Object)x) || rest.contains(x); } }
Example • This program tests whether its first command line argument is repeated in subsequent arguments. • public class TestForDuplicates { public static void main(String[] args) { Set<String> set = new EmptyList(); for (int i = 1; i < args.length; i++) set = set.include(args[i]); System.out.println(set.contains(args[0])); } }
Bounded Polymorphism • Implement sets using a binary tree. • need know how to compare the elements. • have to set up a constraint on our parameter type - bind the type. • Set up an interface to which we'll bind our parameter types: • interface Comparable { boolean equals(Object x); boolean less(Object x); } • interface Set<A implements Comparable> { boolean contains(A x); Set<A> include(A x); } • This means we can only use types that implement Comparable as parameters to the interface Set.
Implement Set using Tree • abstract class Tree<A implements Comparable> implements Set<A> { public abstract boolean contains(A x); public abstract Tree<A> insert (A x); public Set<A> include(A x) { return insert(x); } } • class EmptyTree<A implements Comparable> extends Tree<A> { public boolean contains(A x) { return false; } public Tree<A> insert(A x) { return new Branch(x, this, this); } }
class Branch<A implements Comparable> extends Tree<A> { A elem; Tree<A> left; Tree<A> right; public Branch(A elem, Tree<A> left, Tree<A> right) { this.elem = elem; this.left = left; this.right = right; } public boolean contains(A x) { if (x.less(elem)) return left.contains(x); else if (elem.less(x)) return right.contains(x); else // we assume total ordering, therefore x.equals(elem) return true; }
public Tree<A> insert(A x) { if (x.less(elem)) return new Branch(elem, left.insert(x), right); else if (elem.less(x)) return new Branch(elem, left, right.insert(x)); else // we assume total ordering, therefore x.equals(elem) return this; } }
Use Case • class ComparableString implements Comparable { private String s; ComparableString(String s) { this.s = s; } public boolean less(Object other) { if (other instanceof ComparableString) return s.compareTo( ((ComparableString)other).s) < 0; else throw new Error("can't compare to non-strings"); } } • note: Some type casts are still needed!
Use Case (continued) • public class TestForDuplicates { public static void main(String[] args) { Set<ComparableString> set = new EmptyTree(); for (int i = 1; i < args.length; i++) set.include(new ComparableString(args[i])); System.out.println( set.contains(new ComparableString(args[0]))); } }
F-bounded Polymorphism • Parameterize Comparable interface • interface Comparable<A> { boolean less(A x); } • interface Set<A implements Comparable<A>> { boolean contains(A x); Set<A> include(A x); } • Now ComparableString can be defined as follows: • class ComparableString implements Comparable<ComparableString> { private String s; ComparableString(String s) { this.s = s; } public boolean less(ComparableString other) { return s.compareTo(other.s) < 0; }}
Polymorphic Functions • Type parameter cannot appear at static methods: • abstract class Tree<A implements Comparable> implements Set<A> { public abstract boolean contains(A x); public abstract Tree<A> insert (A x); public Set<A> include(A x) { return insert(x); } public static Tree<A> makeTree(A e) {…} // illegal ! } • (0) Tree<String> t = new Tree(…); t.insert(s1) … • (x) Tree<Integer> t2 = Tree.makeTree( new Integer(s))…
Sometimes, we still need to have polymorphic static functions. Or, need a dynamic method which is polymorphic independently of any type parameters. • This can be done by prefixing the method return type with a type-variable section: • class TreeUtils { static <A implements Comparable<A>> Tree<A> insertArray(Tree<A> tree, A[] elems) { for (int i = 0; i < elems.length; i++) tree = tree.insert(elems[i]); return tree; } }
Implementing parametric types • Two translations are possible • The homogeneous translation map type variables to Objects and inserts appropriate casts. • The heterogeneoustranslation creates many instances of a parameterised class, one for each instance type. • Pizza is designed so that any of the two translations can be used. • explained in more detail in the language definition and the Paper "Two ways to bake your Pizza". • The homogeneous translation effectively erases type information. • This leads to a couple of restrictions on the uses of parameterised types. • In the current Pizza compiler the homogeneous translation is used only!
Restrictions on parametric types • The target type of a coercion must be a Java type -- no type parameters allowed • OK: (Tree)object which is of type Tree<A>, for some unknown type variable A. • Not OK: (Tree<int>)object • The element type of an array creation expression new T[...] must also be a Java type. • OK: new Tree[10] This allocates an array of 10 trees. Their element type is determined by the context. • Not OK: new A[10] where A is a type variable.
First class functions • Use functions as special kinds of data types. • can store a function in a variable, • can access it and • can pass a function as parameter to method without any knowledge about the class it's defined in. • like function pointers in C++. • Can avoid the effort of wrapping classes to make them implement a special interface. • We define all the functions we need to be passed as parameters to our methods. • we pass a function as parameter to a function!)
The syntax • The syntax for function types is:(argtype, ..., argtype) -> resulttype. • Or, if exceptions are thrown by the function:(argtype, ..., argtype) throws exception, ..., exception -> resulttype. • Any method with a matching signature may be substituted for a function parameter. • Ex: • boolean doIt(int, String) matches • (int, String) -> boolean • int read(InputStream) throws IOException matches • (InputStream) throws IOException -> int.
Example class PrintSortedStrings { static void print( String[] args, (String, String) -> int compare) { .... if (compare(args[i], args[j]) > 0) .... } } • cf: • interface Comparator { int compare(String,String); } • class PrintSortedStrings { static void print( String[] args, Comparator comparator) { .... if (comparator.compare(args[i], args[j]) > 0).... } }
To uses the compareTo method of the class String in one case and a case insensitive version in another we can now write: • int myCompare(String s1, String s2){ return s1.compareTo(s2); } • int myCompareNoCase(String s1, String s2){ return s1.toLowerCase().compareTo(s2.toLowerCase()); } • String[] myStrings = { "These", "Strings", "will", "be", "sorted", "!"}; • PrintSortedStrings.print(myStrings,myCompare); • PrintSortedStrings.print(myStrings,myCompareNoCase);
First class function in parametric Types • The use of first class functions becomes much more important if you are using parametric polymorphism. • In the tree example: • needed the method less introduced via an interface. • Refine the class Tree to take a function parameter to the methods contains and insert: • abstract class Tree<A> { public abstract boolean contains( (A,A)->boolean less, A x); public abstract Tree<A> insert( (A,A)->boolean less, A x); } • Note: less is now a function object instead of a Comparable<A> Object.
These methods can now use the variable less to use it's function, as if it was defined locally: public boolean contains((A, A) -> booleanless, A x) { if (less(x, elem)) return left.contains(less, x); else if (less(elem, x)) return right.contains(less, x); else // we assume total ordering, therefore x.equals(elem) return true; }
Reimplement Branch<A> • Not: It is no longer needed to restrict <A> to implement Comparable<A>. • , which was not possible in the F-bounded version via the interface. • class Branch<A> extends Tree<A> { A elem; Tree<A> left; Tree<A> right; public Branch(A elem, Tree<A> left, Tree<A> right) { this.elem = elem; this.left = left; this.right = right; }
public boolean contains((A, A) -> boolean less, A x) { if (less(x, elem)) return left.contains(less, x); else if (less(elem, x)) return right.contains(less, x); else // we assume total ordering, therefore x.equals(elem) return true; } public Tree<A> insert((A, A) -> boolean less, A x) { if (less(x, elem)) return new Branch(elem, left.insert(less, x), right); else if (less(elem, x)) return new Branch(elem, left, right.insert(less, x)); else return this; }}
Tree<String> • Extend the Branch class and overload the methods contains and insert: class StringBranch extends Branch<String> { StringBranch(String elem, Tree<String> left, Tree<String> right) { super(elem, left, right); } private static boolean stringLess(String x, String y) { return x.compareTo(y) > 0; } public boolean contains(String x) { return contains(stringLess, x); } public Tree<String> insert(String x) { return insert(stringLess, x); }}
Anonymous Functions • Instead of defining a method belonging to a class Pizza allows you to define functions locally. • These functions don't have a name that's why they are known as anonymous functions. • Pizza's syntax to define an anonymous function is: fun(arguments)-> resulttype { statements }
Example: anonymous function public class TestForDuplicatesAnonymous { public static void main(String[] args) { Set<String> s = new TreeSet( fun(String x, String y) -> boolean { return x.compareTo(y) < 0; } ); for (int i = 1; i < args.length; i++) s.include(args[i]); System.out.println(s.contains(args[0])); } }
Example • () -> int makeIncr(int start) { int count = start; return fun() -> int { return count++; } } • () -> int incr1 = makeIncr(0); • () -> int incr2 = makeIncr(5); • System.out.println( incr1() + " " + incr1() + " " + incr2() + " " + incr1()); • will return 0 1 5 2.
Traverse a tree • main benefit of first class function. // can replace visitor • void <A> traverse(Tree<A> t, (A)->void proc) { if (t instanceof Branch) { Branch b = (Branch) t; traverse(b.left, proc); proc(b.elem); traverse(b.right, proc); }} • We can use this method to sum up all elements of a tree of integers: • int sum(Tree<int> t) { int n = 0; traverse(t, fun(int x) -> void { n += x; }); return n; }
Algebraic Data Types Shape Circle, Rectangle, Triangle,… void draw(Shape s) { if(s instanceof Circle) { Circle c = (Circle) s; …} else if (s instanceof Rectangle) { Rectangle r = (Rectangle) s; …} else … • Problem: • 1. many type checks and type casts required. • 2. One abstract class many concrete subclasses.
Algebraic data types • Pizza gives us an easy to use syntax to avoid the complicated abstract class/concrete cases construction: • In Pizza class can contain declarations of these forms: • case ident(arguments); • case ident; • With this syntax our Tree class becomes: class Tree<A> implements Set<A> { case Empty; case Branch(Aelem, Tree<A>left, Tree<A>right); }
This example defines one class Tree which • provides the variable Empty representing empty branches and • a method Branch(A, Tree<A>, Tree<A>) instantiating a representation for a non-empty branch. • public class Test { public static void main(String[] args) { Tree<int> t = Branch(2, Branch(1, Empty, Empty), Branch(3, Empty, Empty)); } }
Pattern matching • Pizza internally generates inner classes to represent algebraic cases of a class. • In these classes the arguments we provide in the case syntax become variables. • We could access the element stored in a branch this way: Tree<String> t = ... ; String s; if (t instanceof Branch) { s = (Branch) t.elem; }else { s = "(empty)"; }
Access instances of an algebraic class Pizza provides a very simple way to access instances of an algebraic class similar to Java's switch statement class Tree<A> { case Empty; case Branch(A elem, Tree<A> left, Tree<A> right); public boolean contains((A, A) -> boolean less, A x) { switch (this) { // not legal in java since this is not integral case Empty: return false; case Branch(A elem, Tree<A> l, Tree<A> r): if (less(x, elem)) return l.contains(less, x); else if (less(elem, x)) return r.contains(less, x); else return true; } }
Pattern matching public Tree<A> insert( (A, A) -> boolean less, A x) { switch (this) { case Empty: return Branch(x, Empty, Empty); case Branch(A e, Tree<A> l, Tree<A> r): if (less(x, e)) return Branch(e, l.insert(less, x), r); else if (less(e, x)) return Branch(e, l, r.insert(less, x)); else return this; } }} • switch (var) does two things for us: • 1. select the case corresponding to var. • 2. bind all formal variables with the corresponding fields (l this.left ; r this.right; e this.elem).
Don’t Care fields in the pattern • We can replace parameters we don't want to access with a single '_': Tree<A> goRight() { switch (this) { case Empty: return this; case Branch(_, _, Tree<A> r): return r.goRight(); } }
Deep pattern matching • Pattern matching is not restricted to just one level of variables. • static <A,B> List<C> mapPairs((A,B)->C f, List<A> xs, List<B> ys) { List<C> result; switch (Pair.Pair(xs, ys)) { case Pair(List.Nil, _): case Pair(_, List.Nil): result = List.Nil; break; case Pair( List.Cons(A x, List<A> xs1), List.Cons(B y, List<B> ys1) ): result = List.Cons( f(x, y), mapPairs(f, xs1, ys1)); break; case _: System.out.println("case 2"); break; } return result; }
Special case: Classes with a single constructor • The algebraic class syntax is so convenient that it's useful even if there are no sub-cases. • An example is the class Pair: class Pair<A,B> { case Pair(A fst, B snd);} is a shorthand for class Pair<A,B> { A fst; B snd; Pair(A fst, B snd) { this.fst = fst; this.snd = snd; } } • If there's only one constructor, no separate class for the case will be produced. Therefore, it's OK to use the same name Pair for the class and the case.
Special case: Enums By defining a class with only constant subcases we get enumeration types, which are missing in Java (compared to C): class Color { case Red; case Green; case Blue; String toString() { switch (this) { case Red: return "Red"; case Green: return "Green"; case Blue: return "Blue"; } }} • This is often preferable to using sets of integer constants since a typecheck by the compiler is provided and you can't use an undefined value.
Type casts in Pizza • To use parametric polymorphism even for Java's basic types we sometimes need the ability to cast between objects and basic types. • Pizza provides typecasts from basic types to their boxing classes and back: int basicX; Integer integerX; Object objectX; basicX = (int)integerX; basicX = (int)objectX; integerX = (Integer)basicX; objectX = (Object)basicX; • However, as always typecasts can go wrong during runtime!
Tail Call Recursion continueint veryRecursive(int howDeep) { if (howDeep == 0) { System.out.println("I'm there -- finally!"); return -1; } else returngoto veryRecursive(howDeep - 1); }