Transcript Document

Parametric
Polymorphism
Antonio Cisternino
Parametric Polymorphism
C++ templates implement a form of
parametric polymorphism
 PP is implemented in many flavors and
many languages: Eiffel, Mercury,
Haskell, ADA, ML, C++…
 Improve the expressivity of a language
 May improve the performance of
programs
 It is a form of Universal polymorphism

C++ templates and macros




Macros are dealt by the preprocessor
C++ templates are implemented on the syntax tree
The instantiation strategy is lazy
The following class compiles unless the method foo
is used:
template <class T>class Foo {
T x;
int foo() { return x + 2; }
};
Foo<char*> f;
f.x = “”;
f.foo();
A more semantic approach

Parametric polymorphism has been
introduced also in Java and C#
 Java



Generics and Generic C# for .NET
In both cases the compiler is able to check
parametric classes just looking at their
definition
Parametric types are more than macros on
AST
Syntax for generics is similar in both JG and
C#
Generics in a Nutshell

Type parameterization for classes, interfaces, and
methods e.g.
In GJ is
class<T>
Set<T>
{ ... }
// parameterized class
T[] Slice(…)
class Dict<K,D> { ... }
// two-parameter class
interface IComparable<T> { ... } // parameterized interface
struct Pair<A,B> { ... }
// parameterized struct (“value class”)
T[] Slice<T>(T[] arr, int start, int count) // generic method

Very few restrictions on usage:

Type instantiations can be primitive (only C#) or class e.g.
Set<int>


Dict<string,List<float>>
Pair<DateTime, MyClass>
Generic methods of all kinds (static, instance, virtual)
Inheritance through instantiated types e.g.
class Set<T> : IEnumerable<T>
class FastIntSet : Set<int>
Virtual methods
only in GC#!
More on generic methods




Generic methods are similar to template methods in C++
As in C++ JG tries to infer the type parameters from the
method invocation
C# requires specifying the type arguments
Example:
template <class T> T sqr(T x) { return x*x; }
std::cout << sqr(2.0) << std::endl;
class F { <T> static void sort(T[] a) {…} }
String[] s; F.sort(s);
class F { static void sort<T>(T[] a) {…} }
string[] s; F.sort<string>(s);
C++
JG
C#
Generic Stack
class Stack<T> {
private T[] items;
private int nitems;
Stack<T> { nitems = 0; items = new T[] (50); }
T Pop() {
if (nitems == 0) throw Empty();
return items[--nitems];
}
bool IsEmpty() { return (nitems == 0); }
void Push(T item){
if (items.Length == nitems) {
T[] temp = items;
items = new T[nitems*2];
Array.Copy(temp, items, nitems); }
items[nitems++] = item;
}
}
How does
the compiler
check the
definition?
Tip


C++ requires a space in nested parameter types: vector<vector<int> > to avoid
ambiguity with operator >>
GJ (and C#) fixed the problem with the following grammar:
ReferenceType ::= ClassOrInterfaceType | ArrayType | TypeVariable
ClassOrInterfaceType ::= Name | Name < ReferenceTypeList1
ReferenceTypeList1 ::= ReferenceType1 | ReferenceTypeList , ReferenceType1
ReferenceType1 ::= ReferenceType > | Name < ReferenceTypeList2
ReferenceTypeList2 ::= ReferenceType2 | ReferenceTypeList , ReferenceType2
ReferenceType2 ::= ReferenceType >> | Name < ReferenceTypeList3
ReferenceTypeList3 ::= ReferenceType3 | ReferenceTypeList , ReferenceType3
ReferenceType3 ::= ReferenceType >>>
TypeParameters ::= < TypeParameterList1
TypeParameterList1 ::= TypeParameter1 | TypeParameterList , TypeParameter1
TypeParameter1 ::= TypeParameter > | TypeVariable extends ReferenceType2 |
TypeVariable implements ReferenceType2
The semantic problem



The C++ compiler cannot make assumptions
about type parameters
The only way to type-check a C++ class is to
wait for argument specification (instantiation):
only then it is possible to check operations used
(i.e. comp method in sorting)
From the standpoint of the C++ compiler
semantic module all types are not parametric
Checking class definition





To be able to type-check a parametric class just
looking at its definition we introduce the notion of
bound
As in method arguments have a type, type
arguments are bound to other types
The compiler will allow to use values of such
types as if upcasted to the bound
Example: class Vector<T : Sortable>
Elements of the vector should implement (or
inherit from) Sortable
Example
interface Sortable<T> {
int compareTo(T a);
Not possible in Java,
}
because Sortable is an
class Vector<T : Sortable<T>> {interface and type T is lost.
T[] v;
int sz;
Vector() { sz = 0; v = new T[15]; }
void addElement(T e) {…}
void sort() {
…
if (v[i].compareTo(v[j]) > 0)
…
Compiler can type}
check this because v
}
contains values that
implement Sortable<T>
Pros and Cons




A parameterized type is checked also if no instantiation is
present
Assumptions on type parameters are always explicit (if no
bound is specified Object is assumed)
Is it possible to made assumptions beyond bound?
Yes, you can always cheat by upcasting to Object and then to
whatever you want:
class Foo<T : Button> {
void foo(T b) {
String s = (String)(Object)b;
}
}

Still the assumption made by the programmer is explicit
Implementation




Alternative implementations of parametric
polymorphism:
C++ generates Abstract Syntax Tree for
method and classes
GJ implements generic types at compile
time: the JVM is not aware of parametric
types
C# assumes that CLR is aware of parametric
types: the IL has been extended with generic
instructions to handle with type parameters
Java Generics strategy
JG is an extension of Java
 The compiler verifies that generic types
are used correctly
 Type parameters are dropped and the
bound is used instead; downcasts are
inserted in the right places
 The output is a normal class file
unaware of parametric polymorphism

Example
class Vector<T> {
T[] v; int sz;
Vector() {
v = new T[15];
sz = 0;
}
<U implements Comparer<T>>
void sort(U c) {
…
c.compare(v[i], v[j]);
…
}
}
…
Vector<Button> v;
v.addElement(new Button());
Button b = v.elementAt(0);
class Vector {
Object[] v; int sz;
Vector() {
v = new Object[15];
sz = 0;
}
void sort(Comparer c) {
…
c.compare(v[i], v[j]);
…
}
}
…
Vector v;
v.addElement(new Button());
Button b =
(Button)b.elementAt(0);
Wildcard
class Pair<X,Y> {
X first;
Y second;
}
public String pairString(Pair<?, ?> p) {
return p.first + “, “ + p.second;
}
Expressivity vs. efficiency






JG doesn’t improve execution speed; though it helps
to express genericity better than inheritance
There is a main limit in JG expressivity: at runtime
exact type information is lost
All instantiations of a generic type collapse to the
same class
Consequences are no virtual generic methods and
pathological situations
Benefit: Java classes could be seen as generic
types! Reuse of the large existing codebase
JG isn’t the only implementation of generics for Java
Generics and Java
System
Feature
Parameterized
types
Polymorphic
methods
Type checking at
point of definition
Non-reference
instantiations
Exact run-time
types
Polymorphic virtual
methods
Type parameter
variance
JG/Pizza NextGen
PolyJ
Bracha,
Odersky,
Stoutamire,
Wadler
Bank,
Liskov,
Myers
Cartwright,
Steele


+ bounds

Generic CLR
Agesen,
Freund,
Mitchell
Kennedy,
Syme


+ bounds
+
constraints
+ bounds
+ bounds

















?












Problem with JG
Stack<String> s = new Stack<String>();
s.push("Hello");
Stack<Object> o = s;
Stack<Button> b = (Stack<Button>)o;
// Class cast exception
Cast authorized:
Button mb = b.pop();
both Stack<String> and
Stack<Button> map to class
Stack
Generic C# Strategy: GCLR





Kennedy and Syme have extended CLR to support
parametric types (the same proposal has been made
for PolyJ by Cartwright and Steele)
In IL placeholders are used to indicate type
arguments (!0, !1, …)
The verifier, JIT and loader have been changed
When the program needs an instantiation of a
generic type the loader generates the appropriate
type
The JIT can share implementation of reference
instantiations (Stack<String> has essentially the
same code of Stack<Object>)
Generic C# compiler






GC# compiler implements a JG like notation
for parametric types
Bounds are the same as in JG
NO type-inference on generic methods: the
type must be specified in the call
The compiler relies on GCLR to generate the
code
Exact runtime types are granted by CLR so
virtual generic methods are allowed
All type constructors can be parameterized:
struct, classes, interfaces and delegates.
Example
using System;
namespace n {
public class Foo<T> {
T[] v;
Foo() { v = new T[15]; }
public static
void Main(string[] args) {
Foo<string> f =
new Foo<string>();
f.v[0] = "Hello";
string h = f.v[0];
Console.Write(h);
}
}
}
.field private !0[] v
.method private hidebysig
specialname
rtspecialname
instance void .ctor() cil
managed {
.maxstack 2
ldarg.0
call
instance void
[mscorlib]System.Object::.ct
or()
ldarg.0
ldc.i4.s
15
newarr
!0
stfld
!0[] class
n.Foo<!0>::v
ret
} // end of method Foo::.ctor
Performance




The idea of extending CLR with generic types
seems good; but how about performance?
Although the instantiation is performed at
load time the overhead is minimal
Moreover code sharing reduces
instantiations, improving execution speed
A technique based on dictionaries is
employed to keep track of already
instantiated types
Expressive power of Generics




System F is a typed -calculus with
polymorphic types
While Turing-equivalence is a trivial property
of programming languages; for a type-system
being equivalent to System F it is not
Polymorphic languages such as ML and
Haskell cannot fully express System F (both
languages have been extended to fill the gap)
System F can be transposed into C#
http://www.cs.kun.nl/~erikpoll/ftfjp/2002/Kenne
dySyme.pdf
Reminder: substitutivity



Sub-Typing/Sub-Classing defines the class
relation “B is a sub-type of A”, marked B <: A.
According to the substitution principle, if B <: A,
then an instance of B can substitute an instance
of A.
Therefore, it is legal to assign an instance b of
B to a reference of A
A a = b
Generics and Subtyping

Does the rules for sub-types and assignment
works for generics?
If B <: A, then G<B> <: G<A>?
Counter example
List<String> ls = new List<String>();
List<Object> lo = ls;
// Since String <: Object, so far so good.
lo.add(new Object());
String s = ls.get(0); // Error!
The rule B <: A  G<B> <: G<A> defies the principle of
substitution!
Other example
class B extends A { … }
class G<E> {
public E e;
}
G<B> gb = new G<B>();
G<A> ga = gb;
ga.e = new A();
B b = gb.e; // Error!
Given B <: A, and
assuming G<B> <:
G<A>, then:
G<A> ga = gb;
would be legal.
Actually, type is erased.
Bounded Wildcard
A wildcard does not allow doing much
To provide operations with wildcard types, one can
specify bounds:
Upper Bound
The ancestor of unknown:
G<? extends X>
Lower Bound
The descendant of unknown:
G<? super Y>
Bounded Wildcards Subtyping
Rules
For any B such that B <: A:
 G<B> <: G<? extends A>
 G<A> <: G<? super B>
Bounded Wildcards - Example
G<A> ga = new G<A>();
G<B> gb = new G<B>();
G<? extends A> gea = gb;
// Can read from
A a = gea.e;
G<? super B> gsb = ga;
// Can write to
gsb.e = new B();
G<B> <: G<? extends A>
hence legal
G<A> <: G<? super B>
hence legal
Generics and Polymorphism
class Shape { void draw() {…} }
class Circle { void draw() {…} }
class Rectangle { void draw() {…} }
public void drawAll(Collection<? extends Shape> shapes) {
for (Shape s: shapes)
s.draw();
}
Collection<Shape> will not work. Why?
Lower Bound Example
interface sink<T> {
flush(T t);
}
public <T> T
flushAll(Collection<T> col,
Sink<T> sink)
{
T last;
for (T t: col) {
last = t;
sink.flush(t);
}
return last;
}
Lower Bound Example (2)
Sink<Object> s;
Collection<String> cs;
String str = flushAll(cs, s); // Error!
Lower Bound Example (3)
public <T> T flushAll(Collection<T> col,
Sink<T> sink) { … }
…
String str = flushAll(cs, s); // Error!
T is now solvable as Object, but it is not the
correct type: should be String
Lower Bound Example (4)
public <T> T flushAll(Collection<T> col,
Sink<? Super T> sink) { … }
…
String str = flushAll(cs, s); // OK!
Combining generics and
inheritance

The inheritance relation must be extended
with a new subtyping rule:
Given
class C<T1,...,Tn> extends B
we have
C<t1,...,tn> <: B[t1/T1, ..., tn/Tn]


Can now cast up and down to Object safely
Note: types must be substituted because the
super-class can be parametric
Manipulating types




Grouping values into types has helped us to
build better compilers
Could we do the same with types?
Types can be grouped by means of
inheritance which represents the union of
type sets
Parametric types combined with inheritance
allow expressing function on types:
class Stack<T:object> : Container
Function name
Function arguments
Result type
Example: generic containers
class Row<T : Control> : Control
{ /* row of graphic controls *> }
class Column<T : Control> : Control
{ /* column of graphic controls */ }
class Table<T : Control> : Row<Column<T>>
{ /* Table of graphic controls */ }
…
// It generates the keypad of a calculator
Table<Button> t = new Table<Button>(3, 3);
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
t[i, j].Text = (i * 3 + j + 1).ToString();