Type Joseph Spring Discussion Languages and Type Type Checking Subtypes Type and Inheritance and 7COM1023 Programming Paradigms 1 2 Type Type denotes the kind of values that programs can manipulate: Simple types» Integers, decimal numbers, characters, Boolean values Structured types» Character strings, lists, trees, hash tables More complex types» Functions, classes 3 Very few languages do not partition the collection of data values (bit patterns) into distinct types Assembler language doesn t BCPL (Basic Combined Programming Language) didn t. developed by Martin Richards Uni of Cambridge, 1966 aka the Before C Programming Language The first brace programming language BCPL, followed by B (stripped down, syntactically modified version of BCPL became the basis for C Some languages check type safety: during the compilation stage (static) others while the program is executing. (dynamic) 4 Languages like Modula 2, Ada, Pascal, C++, C# and C are statically typed languages The languages require that a single type is bound to a variable when the variable is declared, remaining bound throughout run time Hence the type of the value of an expression is determined at compile time A language is said to be: statically typed if the types of all variables are fixed when they are declared at compile time Languages like Perl, Python and Scheme are dynamically typed languages The languages allow the type of a variable to be redefined each time a new value is assigned to it at run time To facilitate this a type indicator is stored at run time with each value A language is said to be: Dynamically typed if the type of a variable can vary at run time depending on the value assigned 5 6 1
A programming language is said to be strongly typed if its type system allows all type errors in a program to be detected either at compile time or run time Examples include Ada and Java C and C++ are not considered strongly typed Dynamically typed languages such as Perl and Scheme are strongly typed languages Strongly typed programming generally produces more reliable programs Is seen as a virtue in programming language design Static Type Checking Type checking means that operations or functions can only be applied to arguments of an appropriate type static means consistency is checked before the program is run. To do this all expressions, arguments to functions and, for procedural languages, variables must be given a type. 7 8 Static Type Checking Example For Example: int sum(int a[]) { int sum = 0; for(int i=0; i<a.length; i++) sum=sum+a[i]; return sum; int j, k; k = sum(j); is a compile time type error. 9 Type Inference Some languages like Haskell and SML use type inference which means the user does not need to explicitly give types for all names: parameters and functions the compiler works it out. Consider: sumlist s = if null s then 0 else (head s) + sumlist (tail s) which has type: sumlist :: Num a => [a] > a which means if a is any number type then the function sumlist takes a list of a s and returns a number. Writing code that misuses it will give a type error: (Standard ML (SML) is a general purpose, modular, functional programming language with compile time type checking and type inference) 10 Type Inference Main> sumlist 57 ERROR Cannot infer instance *** Instance : Num [a] *** Expression : sumlist 57 AND all this is done BEFORE the code is executed, ie. by the compiler 11 Dynamic Type Checking Ruby, Python, Lisp, Prolog and others do not have typed variables. Such languages have dynamic type checking. They still prevent incorrect operations on data types but check at runtime. So: (defun sumlist (l) (cond ((null l) 0) (t (+ (car l) (sumlist (cdr l)))) )) 12 2
Dynamic Type Checking Examples of running it: [2]> (sumlist (1 2 3 5 7 11)) 29 [3]> (sumlist 45) *** CAR: 45 is not a list it tried to execute and failed in the middle of the function attempting to take the head (car) of 45 13 Dynamic Type Checking Advantages It provides flexibility and speed of development Disadvantages it is possible to write programs that could sometimes produce type errors but with different data might not do so: (cond ((equal x 10) (sumlist 77)) (t (sumlist (1 2 3)))) this code might execute, it might fail dynamic checking languages must have extra hidden runtime code to check values in variables before EVERY operation on them 14 Subtypes Checking with Subtypes Many languages permit some form of subtype relationship If type t 2 is a subtype of t 1 then values of type t 2 can be used where values of type t 1 are expected. Type t 1 is the parent (base or host) type of t 2 The subtype t 2 has all the characteristics of its parent type and then adds some, for example its range of values in some way restricted 15 Subtypes But all the operations of the parent type are applicable to the child type. Type checking now does not check that the types of arguments or variables are the same but rather that they are compatible ie. are subtypes of the same base type. 16 Subtyping and Inheritance Subtyping is often associated with inheritance in object oriented languages Most treat a subclass as a subtype: class Parent { public void g() { class Child extends Parent { void fun(parent o) { o.g() Child v = new Child(); fun(v); h d i l f Child 17 Subtyping and Inheritance the parameter passed is a value of Child type which is a subtype of the formal argument so it is compatible. Any operation invoked inside g on the value named by o will be defined on the value v. NB it would be illegal to allow Parent values to be used where Child values are expected because the Child class might have additional attributes that would not be present in the Parent value. 18 3
Subranges Another use of subtyping is subrange values used in Ada and some other languages: procedure compat is subtype sta is integer range 0..100; subtype stb is integer range 100..100; i: integer := 3; j : integer; s : sta := 20; t : stb := 10; begin j := s * t; end compat; 19 Subranges Which is OK, s*t is applied between compatible types, both subtypes of integer and the assignment to j is of some subtype value. However Ada also allows the use of a parent type value whenever a child value is expected, this is very odd! In the above example it is legal to assign an integer value to a subrange: s := j; if the value assigned is out of range of left hand side it produces a runtime error 20 is the simplification and generality of having one name to operate safely on values of more than one type For example +, *, apply to ints, doubles in many languages. A function to find the length of a list in Haskell can be applied to any list In 1967 Christopher Strachey identified two principal forms of polymorphism in programming languages: ad hoc polymorphism parametric polymorphism Both these forms of polymorphism are important. They are applicable in different situations 21 ad hoc polymorphism under one name different operations can be applied to different types. This is more commonly called overloading parametric polymorphism one operation can be applied to different types of value. The generic is an example of a form of parametric polymorphism Both these forms of polymorphism are important. They are applicable in different situations 22 is important: Similar operations on different types would need distinct names which would make libraries harder to use Need ad hoc polymorphism (overloading) A monomorphic type system is one where procedures or functions can be applied to only one type. This would mean code must be repeated, a collection would have to be rewritten for different value types Need parametric polymorphism (generics) is important: However dynamic type checked languages offer reuse of code on ANY type of value: (defun length (l) (cond ((null l) 0) (t (+ 1 (length (cdr l)))) )) can be applied to any type of list BUT this is at the cost of no safe compiler checks So this is what a parametric polymorphic type discipline should give: flexibility AND safety. 23 24 4
Flexible Class in Python class Stack: def init (self): self.items = [] def push(self,item): self.items.append(item) def pop(self): return self.items.pop() def isempty(self): return (self.items == []) def top(self): return len(self.items) def str (self): return str(self.items) stck = Stack() stck.push(5) stck.push(10) stck.push(15) sum = 0 while not stck.isempty(): sum = sum + stck.pop() print "sum of stack: ", sum 25 Monomorphic Class in Python class Stack { private int stk[]; private int topp; public Stack(){stk=new int[100]; topp= 1; public void push(int s) { stk[++topp]=s; public void pop() { if(topp>=0) topp=topp 1; public boolean empty() { return topp<0; public int top() throws Exception { if(empty()) throw new Exception("stack empty"); else return stk[topp]; 26 Monomorphic Class in Python Parametric : Generics in Java 1 public class IntStackTest { public static void main(string args[]) throws Exception { Stack lifo = new Stack(); lifo.push(20); lifo.push(30); lifo.push(40); int sum = 0; while(! lifo.empty()) { sum = sum + lifo.top() ; lifo.pop(); System.out.println("sum of stack items " + sum); 27 Consider the Stack class example: class Stack<E> { private E[] mem = (E[]) new Object[4096]; private inttopp; public Stack() { topp = 1; public boolean empty() { return topp<0; E top() { assert(! empty()); return mem[topp]; void pop() { assert(! empty()); topp ; void push(e c) { assert(! full()); topp = topp+1; mem[topp] = c; After this definition stacks can be declared 28 Parametric : Generics in Java 2 After this definition stacks can be declared, but the declarations must provide an actual type for the type parameter: Stack<Integer> istk = new Stack<Integer>(); Stack<String> sstk = new Stack<String>(); and values can be pushed: istk.push(4); istk.push(16); istk.push(24); sstk.push("yellow"); sstk.push("blue"); sstk.push("red"); Given: Parametric : Generics in Java 3 Stack<Integer> istk = new Stack<Integer>(); Stack<String> sstk = new Stack<String>(); the following is NOT allowed: istk.push("pink"); it should give a compilation error. 29 30 5
Parametric in Haskell Consider the simple Haskell function: last s = if null s then error "no last element" else if null (tail s) then (head s) else last (tail s) the function does not depend on the type of the elements of the list the type is: last :: [a] > a where a is a type parameter, the type signature defines all the specific types that can be obtained by substituting any type for the letter a, eg.: [ Int ] > Int [ Float ] > Float [ [Int] ] > [Int] etc. And it is statically type safe, consider: 31 Parametric in Haskell And it is statically type safe, consider: 3 + (last (f x)).. the result of (last (f x)) should be Int because it is an argument to the operator +, the type of last is [a] >a so substituting Int for a (because that s the result) gives the argument to last as [Int] and that can be checked to make sure that s the result of (f x). 32 Parametric : Templates in C++ Parametric : Templates in C++ template <class E> class Stack { private: E mem[256]; int topp; public: Stack(void) { topp= 1; bool empty(void) { return topp<0; bool full(void) { return topp==255; E top(void) { assert(! empty()); return mem[topp]; void pop(void) { assert(! empty()); topp ; void push(e c) { assert(! full()); topp = topp+1; mem[topp] = c; ; 33 Like Java after the definition stacks can be declared, but the declarations must provide an actual type for the type parameter: Stack<int> istk; Stack<string> sstk; and values can be pushed: istk.push(4); istk.push(16); istk.push(24); sstk.push("yellow"); sstk.push("blue"); sstk.push("red"); but NOT: istk.push("pink"); that would, like Java, give a compilation error. But there is a big difference, the instantiation generates a new modified copy of the template. 34 Java before Generics Until version 1.5 Java didn t have a generic (or template) mechanism. Instead it was suggested that containers could hold values of type Object, and since every type is by definition a sub type of Object then by the rules of sub typing the container can hold values of any type. class Stack { protected Object mem[]; protected int topind; public Stack() { mem = new Object[100]; topind= 1; public boolean empty() { return topind<0; public boolean full() { return 99==topind; public Object top() { if(empty()) return null; else return mem[topind]; public void pop() { if(! empty()) topind=topind 1; public void push(object s) { if(! full()) { topind=topind+1; mem[topind]=s; 35 Java before Generics public class BadStackUse { public static void main(string args[]) { Stack filo = new Stack(); int sum = 0; filo.push(new Integer(22)); filo.push(new Integer(33)); filo.push("should be a number"); filo.push(new Integer(55)); System.out.println("Pop contents and add them up"); while(! filo.empty() ) { sum = sum + ((Integer)filo.top()).intValue(); filo.pop(); System.out.println("sum = " + sum); 36 6
Java before Generics Here the Stack objects can contain any type descended from Object, that includes all objects. BUT: when the values are popped off the stack they are only Objects so must be cast to the required type, allowing possible runtime errors! i.e When values are popped off the stack they are Objects (which don t all have an integer value) so they must be cast to Integer and then the int can be retrieved: sum = sum + ((Integer)filo.top()).intValue(); but other stuff, like the String above, can be accidentally pushed on the stack with no static compile time type error. this can cause a runtime type error: rabbit(340)$ java BadStackUse Pop contents and add them up java.lang.classcastexception: at BadStackUse.main(BadStackUse.java:27) 37 Overloading or ad hoc polymorphism Ad hoc polymorphism is different operations on different types but with the same name: the arithmetic operators +, *,, etc. are applicable to more than one type in explicitly programmer typed languages like Java it is simple: the compiler easily determines which instruction to use by the declared or inferred types of its operands user functions (or methods) can be overloaded : int sumsq(int a,int b) { return a*a + b*b; double sumsq(double a, double b) { return a*a + b*b; the compiler can infer which piece of code to use by the context: double x; sumsq(x,3.0) + 2.1.. obviously use the double sumsq function 38 Inheritance and class Base { String name; public Base(String n) { name=n; public String fullname() { return "Base: "+name; class Derived extends Base { public Derived(String n) { super(n); public String fullname() { return "Derived: "+name; public class SubTypeEg01 { public static void main(string args[]) { Base b = new Base("Mummy"); Derived d = new Derived("Baby"); Random rand = new Random(System.nanoTime()); Base bb = rand.nextint(2)==1? d : b; System.out.println( bb.fullname() ); 39 Inheritance and The program defines 2 classes with a subtype relationship, the method fullname() is different in each. Because derived class objects can be used anywhere a parent class object is needed, the variable bb can refer to either type, the compiler cannot know which class of object is referred to by bb when fullname() is called: System.out.println( bb.fullname() ); the runtime system must dynamically select the appropriate version of the method, this is called dynamic binding 40 7