Concepts in Programming Languages
Concepts in Programming Languages
www.cl.cam.ac.uk/teaching/2223/ConceptsPL/
1
Acknowledgement: various slides are based on Marcelo Fiore’s 2013/14
course.
1 / 248
Practicalities
2 / 248
Main books
3 / 248
Context:
so many programming languages
4 / 248
Topics
I. Introduction and motivation.
Part A: Meet the ancestors
II. The first procedural language: FORTRAN (1954–58).
III. The first declarative language: LISP (1958–62).
IV. Block-structured languages: Algol (1958–68), Pascal (1970).
V. Object-oriented languages: Simula (1964–67), Smalltalk (1972).
Part B: Types and related ideas
VI. Types in programming languages: ML, Java.
VII. Scripting Languages: JavaScript.
VIII. Data abstraction and modularity: SML Modules.
Part C: Distributed concurrency, Java lambdas, Scala, Monads
IX. Languages for concurrency and parallelism.
X. Functional-style programming meets object-orientation.
XI. Miscellaneous concepts: Monads, GADTs.
XII. Up-and-coming ideas: Rust, Effect Handlers
5 / 248
˜ Topic I ˜
Introduction and motivation
6 / 248
Goals
7 / 248
Why study programming languages?
8 / 248
What makes a good language?
9 / 248
What makes a language successful?
I Expressive power.
I Ease of use for the novice.
I Ease of implementation.
I Standardisation.
I Many useful libraries.
I Excellent compilers (including open-source)
I Economics, patronage, and inertia.
Note the recent trend of big companies to create/control their
own languages: C# (Microsoft), Hack (Facebook), Go (Google),
Objective-C/Swift (Apple), Rust (Mozilla) and perhaps even
Python (Dropbox hired Guido van Rossum).
10 / 248
? Why are there so many languages?
I Evolution.
I Special purposes.
I No one language is good at expressing all programming
styles.
I Personal preference.
? What makes languages evolve?
I Changes in hardware or implementation platform
I Changes in attitudes to safety and risk
I New ideas from academia or industry
11 / 248
Motivating purpose and language design
A specific purpose or motivating application provides focus for
language design—what features to include and (harder!) what
to leave out. E.g.
I Lisp: symbolic computation, automated reasoning
I FP: functional programming, algebraic laws
I BCPL: compiler writing
I Simula: simulation
I C: systems programming [Unix]
I ML: theorem proving
I Smalltalk: Dynabook [1970-era tablet computer]
I Clu, SML Modules: modular programming
I C++: object orientation
I Java, JavaScript: Internet applications
12 / 248
Program execution model
13 / 248
Classification of programming languages
See en.wikipedia.org/wiki/Programming_paradigm
for more detail:
I Imperative
procedural C, Ada, Pascal, Algol, Fortran, . . .
object-oriented Scala, C#, Java, Smalltalk, SIMULA, . . .
scripting Perl, Python, PHP, JavaScript, . . .
I Declarative
functional Haskell, SML, Lisp, Scheme, . . .
logic Prolog
dataflow Id, Val
constraint-based spreadsheets
template-based XSLT
14 / 248
Language standardisation
15 / 248
Language-standards issues
16 / 248
Language standards: unintended mis-specification
I Function types in Algol 60, see later.
I In language PL/1 the type DEC(p,q) meant a decimal
number of p digits (at most 15) with q digits after the
decimal point, so 9, 8, 3 all had type DEC(1,0).
Division was defined to so that 8/3 was DEC(15,14) with
value 2.666 666 666 666 66.
But addition was defined so that adding these two was also
of type DEC(15,14), which meant that 9 + 8/3 gave
11.666 666 666 666 66, which didn’t fit. This gave either
overflow or the wrong answer of 1.666 666 666 666 66.
I A more recent example is C++11’s “out of thin air”
behaviour, whereby the ISO specification allows the value
42 to appear as the result of a (concurrent) program only
involving assignments of 0 and 1.
Argh! Be careful how you specify a language.
17 / 248
Ultra-brief history
1951–55: Experimental use of expression compilers.
1956–60: Fortran, COBOL, Lisp, Algol 60.
1961–65: APL notation, Algol 60 (revised), SNOBOL, CPL.
1966–70: APL, SNOBOL 4, Fortran 66, BASIC, SIMULA,
Algol 68, Algol-W, BCPL.
1971–75: Pascal, PL/1 (Standard), C, Scheme, Prolog.
1976–80: Smalltalk, Ada, Fortran 77, ML.
1981–85: Smalltalk-80, Prolog, Ada 83.
1986–90: C++, SML, Haskell.
1991–95: Ada 95, TCL, Perl.
1996–2000: Java, JavaScript
2000–05: C#, Python, Ruby, Scala.
1990– : Open/MP, MPI, Posix threads, Erlang, X10,
MapReduce, Java 8 features.
For more information:
en.wikipedia.org/wiki/History_of_programming_
languages
18 / 248
Background Concepts
Modern languages generally have four storage areas
(segments):
I code – usually read-only or execute-only
I static (top-level) variables
I stack – for return addresses, local variables, and
exceptions
I heap
Notes:
I Operating systems also typically provide OS-level threads
which might execute on a separate CPU core. Each OS
thread comes with its own stack.
I Prolog doesn’t quite fit this model as its (WAM) virtual
machine tends to have: a stack for calls, a heap for
allocations, and also a trail stack which manages
backtracking.
19 / 248
Digression: Threads and Heaps
There is a choice here (we will talk a bit more about this later):
I a single heap allocator
I one allocator per thread
The former typically requires locks in the allocator (expensive),
so the second is generally preferred. So how does the allocator
find its next-allocation pointer?
I Another storage area: thread-local statics, thread_local
in C++11; or equivalently:
I A mechanism to find the thread-id for the current thread
and then use it to index a vector to give per-thread storage
areas.
If we have a shared-memory multiprocessor, then we can have
a single heap, but separate efficient per-thread allocators.
On distributed architectures we might have multiple heaps,
perhaps not even cheaply addressable from each other . . . .
20 / 248
Passing arguments to functions
We use the stack (or registers) for this. But there a separate
issue of mechanism:
I call-by-value: callee parameters are effectively local
variables initialised by values of arguments. [default in C,
Java, OCaml],
I call-by-reference: callee parameter is an alias to the
location of the caller argument. [C++ form int &x,
Pascal var, default in Fortran]
I call-by-value-result: caller argument location is read to
initialise local variable in callee (like call-by-value) but also
its final value is copied back.
Exercise: write a program which prints 0, 1 or 2 depending on
which of these is used.
I Some authors use ‘pass-by-value’ etc. rather than
‘call-by-value’.
21 / 248
Passing arguments to functions (2)
22 / 248
Passing arguments to functions (3)
23 / 248
Implementing closures
Note that (in the code above) it’s vital to copy x into the closure
as it will have been deallocated before g and h are called.
24 / 248
C++ lambdas: variable capture (value or reference)
Whether free variables are bound by value or by reference
depends on the language; C++ provides both:
// LLVM use: c++ --std=c++14 to get lambda support
#include <iostream>
int main()
{ int a=0,b=0;
// C++ use of ‘[]’ (lambda)needs to know how
// free variables are bound:
auto f = [a,&b](int x) ->int { return x+a+b; };
a++; b+=10;
std::cout << "f(42)=" << f(42) << std::endl;
// gives "f(42)=52" -- think why...
return 0; }
Notes:
I auto f = [](int x) ->int { return x+a+b;};
gives “error: variable ’a’ cannot be implicitly captured in a
lambda with no capture-default specified.”
I C++ calls the type of f a functor, a class which overloads
operator().
25 / 248
And what about lambdas in Java?
a++; b+=10;
This helps avoid subtle bugs, e.g. where the value of variable is
copied into a closure and the programmer is surprised that
subsequent updates to the variable have no effect on the
closure.
26 / 248
Programming-language phrases – reminder
27 / 248
˜ Part A ˜
28 / 248
˜ Topic II ˜
FORTRAN: A simple procedural language
Further reading:
I The History of FORTRAN I, II, and III by J. Backus. In
History of Programming Languages by R. L. Wexelblat.
Academic Press, 1981.
29 / 248
FORTRAN = FORmula TRANslator (1957)
I Developed (1950s) by an IBM team led by John Backus:
“As far as we were aware, we simply made up the
language as we went along. We did not regard language
design as a difficult problem, merely a simple prelude to
the real problem: designing a compiler which could
produce efficient programs.”
I The first high-level programming language to become
widely used. At the time the utility of any high-level
language was open to question(!), and complaints focused
on efficiency of generated code. This heavily influenced
the design, orienting it towards execution efficiency.
I Standards: 1966, 1977 (FORTRAN 77), 1990 (Fortran 90,
spelling change), . . . , 2018 (Fortran 2018).
I Remains main language for scientific computing.
I Easier for a compiler to optimise than C.
30 / 248
Overview: Compilation
Fortran program = main program + subprograms
I Each is compiled separately from all others.
(Originally no support for cross-module checking, still really
true for C and C++.)
I Translated programs are linked into final executable form.
Fortran program
Compiler
Incomplete machine language Library routines
* w
Linker
Machine language program
31 / 248
Overview: Data types and storage allocation
32 / 248
Overview
Control structures
I FORTRAN 66
Relied heavily on statement labels and GOTO
statements, but did have DO (for) loops.
I FORTRAN 77
Added some modern control structures
(e.g., if-then-else blocks), but WHILE loops and
recursion had to wait for Fortran 90.
I Fortran 2008
Support for concurrency and objects
33 / 248
Example (Fortran 77)
PROGRAM MAIN
PARAMETER (MaXsIz=99)
REAL A(mAxSiZ)
10 READ (5,100,END=999) K
100 FORMAT(I5)
IF (K.LE.0 .OR. K.GT.MAXSIZ) STOP
READ *,(A(I),I=1,K)
PRINT *,(A(I),I=1,K)
PRINT *,’SUM=’,SUM(A,K)
GO TO 10
999 PRINT *, ’All Done’
STOP
END
C SUMMATION SUBPROGRAM
FUNCTION SUM(V,N)
REAL V(N)
SUM = 0.0
DO 20 I = 1,N
SUM = SUM + V(I)
20 CONTINUE
RETURN
END
34 / 248
Example
Commentary
35 / 248
I Data formats for I/O.
I Historically functions are compiled separately from the
main program with no consistency checks. Failure may
arise (either at link time or execution time) when
subprograms are linked with main program.
Fortran 90 provides a module system.
I Function parameters are uniformly transmitted by
reference (like C++ ‘&’ types).
But Fortran 90 provided INTENT(IN) and INTENT(OUT)
type qualifiers and Fortran 2003 added pass-by-value for C
interoperability.
I Traditionally all allocation is done statically.
But Fortran 90 provides dynamic allocation and recursion.
I A value is returned in a Fortran function by assigning a
value to the name of a function.
36 / 248
Program consistency checks
37 / 248
Fortran lives!
38 / 248
˜ Topic III ˜
LISP: functions, recursion, and lists
39 / 248
LISP = LISt Processing (circa 1960)
40 / 248
Some contributions of LISP
41 / 248
Overview
42 / 248
I Programs represented as S-expressions are evaluated to
give values, treating atoms as variables to be looked up in
an environment, and lists as a function (the first element of
the list) to be called along with its arguments (the rest of
the list).
Example:
(APPEND (QUOTE (FOO 1 Y)) (CONS 3 (CONS ’Z NIL)))
evaluates to (FOO 1 Y 3 Z).
Note: the functions CONS and APPEND behaves as in ML,
and the function QUOTE returns its argument unevaluated.
Numbers and the atoms NIL and T (also used as
booleans) evaluate to themselves.
I To ease typing LISP programs, (QUOTE e) can be
abbreviated ’e, see ’Z above.
This is done as part of the READ function which reads
values (which of course can also be programs).
43 / 248
? How does one recognise a LISP program?
( defvar x 1 ) val x = 1 ;
( defun g(z) (+ x z) ) fun g(z) = x + z ;
( defun f(y) fun f(y)
( + ( g y ) = g(y) +
( let let
( (x y) ) val x = y
( in
g x ) g(x)
) ) ) end ;
( f (+ x 1) ) f(x+1) ;
44 / 248
Core LISP primitives
The following primitives give enough (Turing) power to construct
any LISP function, along with top-level DEFUN
I CONS, CAR, CDR: cons, hd, tl.
I CONSP, ATOM: boolean tests for being a cons cell or atom.
I EQ: boolean equality test for equal atoms (but beware using
it on large numbers which may be boxed, cf. Java boxing).
I QUOTE: return argument unevaluated.
I COND: conditional expression.
Example:
(defun subst (x y z)
(cond ((atom z) (cond ((eq z y) x) (T z)))
(T (cons (subst x y (car z))
(subst x y (cdr z))))
)
)
45 / 248
Static and dynamic scoping (or binding)
Consider:
46 / 248
Programs as data
47 / 248
Parameter passing in LISP
I Function parameters are transmitted either all by value or
all by text (unevaluated expressions); only built-in functions
(such as QUOTE, LAMBDA, COND) should really use
pass-by-text. Why: because we need a special variant of
EVAL to evaluate the arguments to COND in the calling
environment, and similarly need to capture the free
variable of a LAMBDA in the environment of the caller.
I The actual parameters in a function call are always
expressions, represented as list structures.
I Note that call-by-text (using either a special form, or explicit
programmer use of QUOTE, and with EVAL to get the value
of an argument) resembles call-by-name (using LAMBDA to
pass an unevaluated expression, and with APPLY to get the
value of an argument), but is only equivalent if the EVAL
can evaluate the argument in the environment of the
function call!
48 / 248
Dangers of eval
In LISP, I can write: (eval ’(+ x 1))
If you’ve been to IB Semantics then your first question should
be “where do I lookup the value of x?” This is a real issue –
should the following give 11 or 12?
(defvar x 1)
(defvar e ’(add x 10))
(defun f (x) (eval e))
(f 2)
Unless you’re very careful any use of eval involving user input
produces a security hole. Consider: (eval (read)) and inputs
like (fire-all-missiles) or (+ 1 top-security-code).
This applies to any language with a one-argument eval. When
using Python (where eval takes a string), use the (sadly
optional) additional environment-specifying parameters.
49 / 248
˜ Topic IV ˜
Block-structured procedural languages
Algol, Pascal and Ada
50 / 248
Sample Pascal program: The keyword var indicates
call-by-reference.
program main;
begin
function f(var x, y: integer): integer;
begin
x := 2;
y := 1;
if x = 1 then f := 3 else f:= 4
end;
var z: integer;
z := 0;
writeln( f(z,z) )
end
51 / 248
Evolution of parameter-passing mechanisms
I Algol 60 supported call-by-value and call-by-name.
Call-by-name was found to be inefficient and not popular
with programmers – despite its lambda-calculus heritage of
beta-reduction.
I A parameter in Pascal is normally passed by value. It is
passed by reference, however, if the keyword var appears
before the declaration of the formal parameter.
procedure proc(x: Integer; var y: Real);
I Ada supports three kinds of parameters:
1. in parameters, corresponding to value parameters;
2. out parameters, corresponding to just the copy-out phase
of call-by-value/result; and
3. in out parameters, corresponding to either reference
parameters or value/result parameters, at the discretion of
the implementation.
I Beware of passing arrays or large structures by value!
52 / 248
Algol
53 / 248
Algol innovations
I Use of BNF syntax description.
I Block structure.
I Scope rules for local variables.
I Dynamic lifetimes for variables.
I Nested if-then-else expressions and statements.
I Recursive subroutines.
I Call-by-value and call-by-name arguments.
I Explicit type declarations for variables.
I Static typing.
I Arrays with dynamic bounds.
I Functions (closures) as arguments – but entertainingly not
“closures as results” as Algol 60 had no heap and so
closures were stack-allocated.
54 / 248
Algol 60
Features
55 / 248
Algol 60
Some trouble-spots
56 / 248
Algol 68
I Algol 68 contributed a regular, systematic type system.
The types (referred to as modes in Algol 68) are either
primitive (int, real, complex, bool, char, string, bits,
bytes, semaphore, format, file) or compound (array,
struct, union, procedure, set, pointer).
Type constructors could be combined without restriction –
a more systematic type system than previous languages.
I Algol 68 used a stack for local variables and heap storage.
Heap data are explicitly allocated, and are reclaimed by
garbage collection.
I Algol 68 parameter passing is by value, with
pass-by-reference accomplished by pointer types. (This is
essentially the same design as that adopted in C.)
I Too complicated (both linguistically and to compile) for its
time.
57 / 248
Pascal (1970)
58 / 248
Pascal variant records
59 / 248
Pascal variant records introduced weaknesses into its type
system.
I Compilers do not usually check that the value in the tag
field is consistent with the state of the record.
I Tag fields are optional. If omitted, no checking is possible
at run time to determine which variant is present when a
selection is made of a field in a variant.
C still provides this model with struct and union. Modern
languages provide safe constructs instead (think how a
compiler can check for appropriate use):
I ML provides datatype and case to express similar ideas.
In essence the constructor names provide the
discriminator k but this is limited to being the first
component of the record.
I Object-oriented languages provide subclassing to capture
variants of a class.
See also the ‘expression problem’ discussion (slide 196).
60 / 248
˜ Topic V ˜
Object-oriented languages: concepts and origins
SIMULA and Smalltalk
61 / 248
Basic concepts in object-oriented languages
62 / 248
Dynamic lookup
I Dynamic lookup2 means that when a method of an object
is called, the method body to be executed is selected
dynamically, at run time, according to the implementation
of the object that receives the message (as in Java or C++
virtual methods).
I For the idea of multiple dispatch (not on the course), rather
than the Java-style (or single) dispatch, see
http://en.wikipedia.org/wiki/Multiple_dispatch
Abstraction
I Abstraction means that implementation details are hidden
inside a program unit with a specific interface. For objects,
the interface usually consists of a set of methods that
manipulate hidden data.
2
Also called ‘dynamic dispatch’ and occasionally ‘dynamic binding’ (but
avoid the latter term as ‘dynamic scoping’ is quite a different concept).
63 / 248
Subtyping
64 / 248
Inheritance
I Inheritance is the ability to reuse the definition of one kind
of object to define another kind of object.
I The importance of inheritance is that it saves the effort of
duplicating (or reading duplicated) code and that, when
one class is implemented by inheriting from another,
changes to one affect the other. This has a significant
impact on code maintenance and modification.
NB: although Java treats subtyping and inheritance as
synonyms, it is quite possible to have languages which have
one but not the other.
I A language might reasonably see int as a subtype of
double but there isn’t any easy idea of inheritance here.
I One can simulate inheritance (avoiding code duplication)
with C-style #include, but this doesn’t add subtyping to C.
I Cook et al. write in Inheritance is not subtyping:
“Subtyping is a relation on interfaces, inheritance is a
relation on implementations”
65 / 248
Behavioural Subtyping – ‘good subclassing’
Consider two classes for storing collections of integers:
class MyBag {
protected ArrayList<Integer> xs;
public void add(Integer x) { xs.add(x); }
public int size() { return xs.size(); }
// other methods here...
}
class MySet extends MyBag
@override
public void add(Integer x) { if (!xs.contains(x))
xs.add(x); }
}
66 / 248
Behavioural Subtyping – ‘good subclassing’ (2)
It shouldn’t really be a subtype, because it violates behavioural
subtyping – members of a subtype should have the same
behaviour as the members of the supertype. Consider:
int foo(MyBag b) { int n = b.size();
b.add(42);
return b.size() - n; }
67 / 248
Behavioural Subtyping – ‘good subclassing’ (3)
OK, so but perhaps it works the other way? Perhaps MyBag is a
behavioural subtype of MySet?
I Yes? foo gives 0 or 1 for supertype and 1 for subtype.
I No: e.g. add is idempotent for MySet but not for MyBag.
I It’s a good idea to document the invariants classes expect
their subclasses to maintain.
OK, so how do I code it?
I Good design would have MyBag and MySet separately
inheriting from a common superclass (or separately
implementing a common interface).
Java 16 adds sealed classes to allow programmers to specify
which other classes can extend them (this is more powerful
than final which is all-or-none). This is useful to forbid a
(rogue) distant piece of code from extending class C to give
behaviours which the coder of class C and its planned
subclasses did not anticipate.
68 / 248
History of object-oriented languages
SIMULA and Smalltalk
69 / 248
I A generic event-based simulation program (pseudo-code):
Q := make_queue(initial_event);
repeat
select event e from Q
simulate event e
place all events generated by e on Q
until Q is empty
naturally requires:
I A data structure that may contain a variety of kinds
of events. subtyping
I The selection of the simulation operation according to
the kind of event being processed. dynamic lookup
I Ways in which to structure the implementation of
related kinds of events. inheritance
70 / 248
SIMULA: Object-oriented features
I Objects: A SIMULA object is a (heap-allocated) activation
record produced by call to a class.
I Classes: A SIMULA class is a procedure that returns a
pointer to its activation record. The body of a class may
initialise the objects it creates.
I Dynamic lookup: Operations on an object are selected
from the activation record of that object.
I Abstraction: Hiding was not provided in SIMULA 67; it was
added later and inspired the C++ and Java designs.
I Subtyping: Objects are typed according to the classes that
create them. Subtyping is determined by class hierarchy.
I Inheritance: A SIMULA class could be defined, by class
prefixing, to extend an already-defined class including the
ability to override parts of the class in a subclass.
71 / 248
SIMULA: Sample code
72 / 248
CLASS LINE(A,B,C); REAL A,B,C;
COMMENT***Ax+By+C=0 REPRESENTATION
BEGIN
BOOLEAN PROCEDURE PARALLELTO(L); REF(LINE) L;
IF L =/= NONE THEN
PARALLELTO := ABS( A*L.B - B*L.A ) < 0.00001;
REF(POINT) PROCEDURE MEETS(L); REF(LINE) L;
BEGIN REAL T;
IF L =/= NONE and ~PARALLELTO(L) THEN
BEGIN
...
MEETS :- NEW POINT(...,...);
END;
END;***MEETS***
COMMENT*** INITIALISATION CODE (CONSTRUCTOR)
REAL D;
D := SQRT( A**2 + B**2 )
IF D = 0.0 THEN ERROR ELSE
BEGIN
A := A/D; B := B/D; C := C/D;
END;
END***LINE***
NB: SIMULA 67 did not hide fields. Thus anyone can change
the colour of the point referenced by CP:
CP.C := BLUE;
74 / 248
SIMULA: Object types and subtypes
I All instances of a class are given the same type. The name
of this type is the same as the name of the class.
I The class names (types of objects) are arranged in a
subtype hierarchy corresponding exactly to the subclass
hierarchy.
I The Algol-60-based type system included explicit REF
types to objects.
75 / 248
Subtyping Examples – essentially like Java:
1. CLASS A; A CLASS B;
REF(A) a; REF(B) b;
a :- b; COMMENT***legal since B is
***a subclass of A
...
76 / 248
Smalltalk
77 / 248
Smalltalk Example
Most implementations of Smalltalk are based around an IDE
environment (“click here to add a method to a class”). The
example below uses GNU Smalltalk which is terminal-based
with st> as the prompt.
st> Integer extend [
myfact [
self=0 ifTrue: [^1] ifFalse: [^((self-1) myfact) * self]
] ]
st> 5 myfact
120
80 / 248
˜ Topic VI ˜
Types in programming languages
Additional Reference:
I Sections 4.9 and 8.6 of Programming languages:
Concepts & constructs by R. Sethi (2 ND EDITION).
Addison-Wesley, 1996.
81 / 248
Types in programming
82 / 248
Type systems
83 / 248
Type safety
A programming language is type safe if no program is allowed
to violate its type distinctions.
85 / 248
Static and dynamic type checking
Run-time (dynamic) type checking:
I Compiler generates code, typically adding a ‘tag’ field to
data representations, so types can be checked at run time.
I Examples: LISP, Smalltalk.
(We will look at dynamically typed languages more later.)
Compile-time (static) type checking:
I Compiler checks the program text for potential type errors
and rejects code which does not conform (perhaps
rejecting code which would execute without error).
I Examples: SML, Java.
I Pros: faster code, finds errors earlier (safety-critical?).
I Cons: may restrict programming style.
86 / 248
Java Downcasts
Consider the following Java program:
class A { ... }; A a;
class B extends A { ... }; B b;
I Variable a has Java type A whose valid values are all those
of class A along with those of all classes subtyping class A
(here just class B).
I Subtyping determines when a variable of one type can be
used as another
√ (here used by assignment):
a = b; √(upcast)
a = (A)b; (explicit upcast)
b = a; √ (implicit downcast—illegal Java)
×
b = (B)a; (but needs run-time type-check)
I Mixed static and dynamic type checking!
See also the later discussion of subtype polymorphism.
87 / 248
Type equality
When type checking we often need to know when two types are
equal. Two variants of this are structural equality and nominal
equality.
Let t be a type expression (e.g. int * bool in ML) and make
two type definitions
type n1 = t; type n2 = t;
88 / 248
Examples:
I Type equality in C/C++. In C, type equality is structural for
typedef names, but nominal for structs and unions;
note that in
struct { int a; } x; struct { int a; } y;
there are two different (anonymously named) structs so x
and y have unequal types (and may not be assigned to
one another).
I Type equality in ML. ML works very similarly to C/C++,
structural equality except for datatype names which are
only equivalent to themselves.
I Type equality in Pascal/Modula-2. Type equality was left
ambiguous in Pascal. Its successor, Modula-2, avoided
ambiguity by defining two types to be compatible if
1. they are the same name, or
2. they are s and t, and s = t is a type declaration, or
3. one is a subrange of the other, or
4. both are subranges of the same basic type.
89 / 248
Type declarations
90 / 248
Type compatibility and subtyping
We might want to allow implicit casts of age to int but not int
to age, and certainly not x := y;.
91 / 248
Polymorphism
Polymorphism [Greek: “having multiple forms”] refers to
constructs that can take on different types as needed. There
are three main forms in contemporary programming languages:
I Parametric (or generic) polymorphism. A function may
be applied to any arguments whose types match a type
expression involving type variables.
Subcases: ML has implicit polymorphism, other languages
have explicit polymorphism where the user must specify
the instantiation (e.g. C++ templates, and the type system
of “System F”).
I Subtype polymorphism. A function expecting a given
class may be applied to a subclass instead. E.g. Java,
passing a String to a function expecting an Object.
[Not covered:] bounded subtype polymorphism combines
aspects of these (e.g. allowing us to restrict the argument to a
polymorphic class to being a subclass or superclass of some
given class).
92 / 248
I Ad-hoc polymorphism or overloading. Two or more
implementations with different types are referred to by the
same name. E.g. Java, also addition is overloaded in SML
(which is why fn x => x+x does not type-check).
(Remark 1: Haskell’s type classes enable rich overloading
specifications. These allow functions be to implicitly
applied to a range of types specified by a Haskell type
constraint.)
(Remark 2: the C++ rules on how to select the ‘right’
variant of an overloaded function are arcane.)
Although we’ve discussed these for function application, it’s
important to note that Java generics and ML parameterised
datatypes (e.g. Map<Key,Val> and ’a list) use the same
idea for type constructors.
93 / 248
Type inference
94 / 248
Type inference in ML – idea
Idea: give every expression a new type variable and then emit
constraints α ≈ β whenever two types have to be equal.
These constraints can then be solved with Prolog-style
unification.
For more detail see Part II course: “Types”.
if x : τ in Γ
Γ`x :τ
Inference rule:
γ ≈ α if x : α in Γ
Γ`x :γ
95 / 248
Typing rule (application):
Γ ` f : σ −> τ Γ`e:σ
Γ ` f (e) : τ
Inference rule:
Γ`f :α Γ`e:β
α ≈ β −> γ
Γ ` f (e) : γ
Inference rule:
Γ, x : α ` e : β
γ ≈ α −> β
Γ ` (fn x => e) : γ
96 / 248
Example:
√ √
√
f : α 1 , x : α3 ` f : α7 f : α1 , x : α3 ` x : α8
f : α 1 , x : α3 ` f : α5 f : α1 , x : α3 ` f (x) : α6
f : α1 , x : α3 ` f (f (x)) : α4
f : α1 ` fn x => f (f (x)) : α2
` fn f => fn x => f (f (x)) : α0
97 / 248
let-polymorphism
98 / 248
Surprises/issues in ML typing
The mutable type ’a ref seemingly(!) has three operators
ref : ’a -> ’a ref
(!) : ’a ref -> ’a
(:=) : ’a ref * ’a -> unit
101 / 248
Interaction of subtyping and generics—variance
In Java, we have that String is a subtype of Object.
I But should String[] be a subtype of Object[]?
I And should ArrayList<String> be a subtype of
ArrayList<Object>?
I What about Function<Object,String> being a subtype
of Function<String,Object>?
Given generic G we say it is
I covariant if G<String> is a subtype of G<Object>.
I contravariant if G<Object> is a subtype of G<String>.
I invariant or non-variant if neither hold.
I variance is a per-argument property for generics taking
multiple arguments .
But what are the rules?
102 / 248
Java arrays are covariant
The Java language decrees so. Hence the following code
type-checks.
String[] s = new String[10];
Object[] o;
o = s; // decreed to be subtype
o[5] = "OK so far";
o[4] = new Integer(42); // whoops!
103 / 248
Java generics are invariant (by default)
The Java language decrees so. Hence the following code now
fails to type-check.
ArrayList<String> s = new ArrayList(10);3
ArrayList<Object> o;
o = s; // fails to type-check
o.set(5,"OK so far"); // type-checks OK
o.set(4, new Integer(42)); // type-checks OK
3
Legal note: it doesn’t matter here, but to exactly match the previous
array-using code I should populate the ArrayList with 10 NULLs. Real code
would of course populate both arrays and ArrayLists with non-NULL values.
104 / 248
Java variance specifications
105 / 248
Java variance specifications (2)
106 / 248
Java variance specifications (3)
[Non-examinable]
I Java has use-site variance specifications: we can declare
variance at every use of a generic.
I Some other languages instead have declaration-site
variance which many find simpler.
Scala has declaration-site variance: the Scala definition
class Array[A] {
def apply( index: Int ): A // OK
def update( index:Int , elem: A ) // see below
... }
is invariant, but could be specified to be covariant (at its
declaration site) by writing:
class Array[+A] { .. }
But doing so would fault argument elem of update.
Similarly, contravariant subtyping is specified with “[-A]”.
Kotlin uses in for + and out for +.
107 / 248
˜ Topic VII ˜
Scripting Languages and Dynamic Typing
108 / 248
Scripting languages
From this definition it’s clear that many (but not all) scripting
languages will have a rather ad-hoc set of features – and
therefore tend to cause computer scientists to have strong
views about their design (indeed it’s arguable that we don’t
really teach any scripting languages on the Tripos).
109 / 248
Scripting languages (2)
110 / 248
Scripting languages (3)
111 / 248
Dynamically typed languages
We previously said:
I Using types to organise a program makes it easier
for someone to read, understand, and maintain the
program. Types can serve an important purpose in
documenting the design and intent of the program.
So why would anyone want to lose these advantages?
I And why is JavaScript one of the most-popular
programming languages on surveys like RedMonk?
Perhaps there is a modern-politics metaphor here, about elites
using Java and ordinary programmers using JavaScript to be
free of the shackles of types?
112 / 248
Questions on what we teach vs. real life
113 / 248
RedMonk language rankings 2022
Rank RedMonk (2022) Rank RedMonk (2022)
1 JavaScript 11 Swift
2 Python 12= R
3 Java 12= Objective-C
4 PHP 14 Shell
5 C# 15= Scala
6 CSS 15= Go
7= C++ 17= PowerShell
7= TypeScript 17= Kotlin
9 Ruby 19= Rust
10 C 19= Dart
Note 1. CSS (Cascading Style Sheets) not really a language.
Note 2. Swift is Apple’s language to improve on Objective C.
Note 3. Don’t trust such surveys too much.
114 / 248
What are all these languages?
115 / 248
JavaScript
116 / 248
Browsers: Java Applets and JavaScript
There are two ways to execute code in a browser:
I [Java Applets] compile a Java application to JVM code and
store it on a web site. A web page references it via the
<applet> tag; the JVM code is run in a browser sandbox.
In principle the best solution, but historically beset with
security holes, and unloved by Microsoft and Apple for
commercial reasons. Effectively dead by 2017.
I [JavaScript] the browser contains a JavaScript interpreter.
JavaScript programs are embedded in source form in a
web page with the <script> tag, and interpreted as the
page is rendered (now sometimes JIT compiled).
Interaction between browser and program facilitated with
the DOM model. Questionable confidentiality guarantees.
https://en.wikipedia.org/wiki/Java_applet
https://en.wikipedia.org/wiki/JavaScript
117 / 248
The JavaScript language
118 / 248
Problems with classes
119 / 248
Prototypes instead of classes
I In JavaScript (and Self), there are no classes.
I When a constructor function is defined, it acquires a
prototype property (the JS for ‘field’ and ‘method’) initially
an empty object.
I When a constructor is called, created objects inherit from
the prototype (i.e. field and method lookup look first in the
object and then in the prototype).
I You can add properties to the prototype object, or indeed
replace the it entirely with another object.
I This gives a prototype inheritance chain similar to a class
inheritance chain.
I The Self community argue that this is a more flexible style
of programming, but many JavaScript systems start by
defining prototypes which play the role of classes, and
copying from these plays the role of new.
120 / 248
Prototypes example
[Taken from the ‘prototype’ section of
https://www.w3schools.com/js/]
121 / 248
Duck typing
If we have classes, we can use Java-style type-testing:
if (x instanceof Duck} { ... }
123 / 248
JavaScript interaction within the browser
124 / 248
WebAssembly – turning full circle?
125 / 248
Some other scripting Languages
126 / 248
Gradual type systems
Dynamically typed languages are great (at least for small
programs):
I “You can write your whole prototype application in Python
while a Java programmer is still writing his/her class
hierarchy”
Statically typed languages are better for large programs, due to
the documentation and cross-module static checks, provided by
types.
I Imagine the discussions at Facebook when management
started to confront the issue that they had one million lines
of PHP in their system.
So what you might want is:
I a language in which small programs can be written without
types, but the IDE encourages you to add types faster than
software rust overtakes your system – gradual types.
127 / 248
Gradual type systems (2)
“Gradual typing is a type system in which some variables and
expressions may be given types and the correctness of the
typing is checked at compile-time (which is static typing) and
some expressions may be left untyped and eventual type errors
are reported at run-time (which is dynamic typing). Gradual
typing allows software developers to choose either type
paradigm as appropriate, from within a single language. In
many cases gradual typing is added to an existing dynamic
language, creating a derived language allowing but not
requiring static typing to be used. In some cases a language
uses gradual typing from the start.” [Wikipedia]
Example languages:
I Microsoft’s TypeScript
I Facebook’s Hack (for PHP)
I Cython and mypy (for Python)
I C#’s dynamic type.
129 / 248
˜ Topic VIII ˜
Data abstraction and modularity
SML Modules
Additional Reference:
I Chapter 7 of ML for the working programmer (2 ND
EDITION ) by L. C. Paulson. CUP, 1996.
May also be useful:
I Purely Functional Data Structures by Chris Okasaki,
Cambridge University Press. [Clever functional data
structures, implemented in Haskell and SML Modules.]
I Previous versions of this course (available on the teaching
pages) had more information on SML Modules.
I http://www.standardml.org/
130 / 248
Need for modules
I We want control over name visibility in large systems.
I Name visibility is important because it expresses
large-scale architecture (which components can use which
other components). Excessive visibility increases the
attack surface for malevolent use.
I Traditional Java does quite well on class-level visibility
(private, protected, public) – but less well on package-level
visibility (no formal way to control who might use a
package-level export, or malevolent uses of reflection).
I Java 9 added a module system in 2017.
I We focus on the SML module system (seminal and
well-designed). OCaml’s module system is similar.
There are also issues (only briefly mentioned) concerning
versioning, when modules evolve over time and multiple
versions are available, e.g. Linux packages or Windows DLLs.
131 / 248
The Core and Modules languages
132 / 248
The Modules language
The SML Modules language lets one split large programs into
separate units with descriptive interfaces.
133 / 248
SML Modules
Signatures and structures
134 / 248
Structures
135 / 248
Concrete signatures
Signatures specify the ‘types’ of structures by listing the
specifications of their components.
137 / 248
Naming structures and signatures
The structure keyword binds a structure to an identifier:
structure IntNat =
struct type nat = int
...
fun iter b f i = ...
end
138 / 248
Modules and dot notation
139 / 248
Example: Polymorphic functional stacks.
signature STACK =
sig
exception E
type ’a reptype (* <-- opaque *)
val new: ’a reptype
val push: ’a -> ’a reptype -> ’a reptype
val pop: ’a reptype -> ’a reptype
val top: ’a reptype -> ’a
end ;
140 / 248
val e = MyStack.new ;
val s0 = MyStack.push 0 e ;
val s01 = MyStack.push 1 s0 ;
val s0’ = MyStack.pop s01 ;
MyStack.top s0’ ;
Gives:
val e = [] : ’a MyStack.reptype
val s0 = [0] : int MyStack.reptype
val s01 = [1,0] : int MyStack.reptype
val s0’ = [0] : int MyStack.reptype
val it = 0 : int
141 / 248
Signature matching
Q: When does a structure satisfy a signature?
A: The type of a structure matches a signature whenever it
implements at least the components of the signature.
I The structure must realise (i.e. define) all of the opaque
type components in the signature.
I The structure must enrich this realised signature,
component-wise:
? every concrete type must be implemented equivalently;
? every value must have a more general type scheme;
? every structure must be enriched by a substructure.
Signature matching supports a form of subtyping not found in
the Core language:
I A structure with more type, value, and structure
components may be used where fewer components are
expected.
I A value component may have a more general type scheme
than expected.
142 / 248
Properties of signature matching
143 / 248
Using signatures to restrict access
144 / 248
Transparency of “_ : _”
145 / 248
Opaque signature constraints: “_ :> _”
Using the ‘:>’ syntax, instead of the ‘:’ syntax used earlier,
signature matching can also be used to enforce data
abstraction by hiding the identity of types:
structure AbsNat = IntNat :> sig
type nat
val zero: nat
val succ: nat->nat
val ’a iter: ’a->(’a->’a)->nat->’a
end
The constraint str :> sgn prunes str but also generates a
new, abstract type for each opaque type in sgn.
146 / 248
I The actual implementation of AbsNat.nat by int is
hidden, so that AbsNat.nat 6= int.
AbsNat is just IntNat, but with a hidden type
representation.
I AbsNat defines an abstract datatype of natural numbers:
the only way to construct and use values of the abstract
type AbsNat.nat is through the operations, zero, succ,
and iter.
E.g., the application AbsNat.succ(˜3) is ill-typed: ˜3 has
type int, not AbsNat.nat. This is what we want, since ˜3
is not a natural number in our representation.
In general, abstractions can also prune and specialise
components.
I Remark: abstractions generalise the expressive power of
the Core SML abstype declaration (this works like
datatype, but does not export its constructors, and so
also hides the representation of a type).
147 / 248
Information hiding
Opaque signature constraints
val e = MyOpaqueStack.new ;
val s0 = MyOpaqueStack.push 0 e ;
val s01 = MyOpaqueStack.push 1 s0 ;
val s0’ = MyOpaqueStack.pop s01 ;
MyOpaqueStack.top s0’ ;
Gives:
val e = - : ’a MyOpaqueStack.reptype
val s0 = - : int MyOpaqueStack.reptype
val s01 = - : int MyOpaqueStack.reptype
val s0’ = - : int MyOpaqueStack.reptype
val it = 0 : int
149 / 248
Structures and signatures can be nested
structure IntNatAdd =
struct
structure Nat = IntNat
fun add n m = Nat.iter m Nat.succ n
end
...
signature Add =
sig structure Nat: NAT
val add: Nat.nat -> Nat.nat -> Nat.nat
end
152 / 248
Example: Generic imperative stacks.
signature STACK =
sig
type itemtype
val push: itemtype -> unit
val pop: unit -> unit
val top: unit -> itemtype
end ;
exception E ;
functor Stack( T: sig type atype end ) : STACK =
struct
type itemtype = T.atype
val stack = ref( []: itemtype list )
fun push x
= ( stack := x :: !stack )
fun pop()
= case !stack of [] => raise E
| _::s => ( stack := s )
fun top()
= case !stack of [] => raise E
| t::_ => t
end ;
153 / 248
structure intStack
= Stack(struct type atype = int end) ;
intStack.push(0) ;
intStack.top() ;
intStack.pop() ;
intStack.push(4) ;
Gives:
val it = () : unit
val it = 0 : intStack.itemtype
val it = () : unit
val it = () : unit
154 / 248
Why functors ?
Functors support:
Code reuse.
AddFun may be applied many times to different
structures, reusing its body.
Code abstraction.
AddFun can be compiled before any
argument is implemented.
Type abstraction.
AddFun can be applied to different types N.nat.
But there are various complications:
I Should functor application be applicative or generative?
I We need some way of specifying types as being shared.
155 / 248
Functors: generative or applicative?
The following functor is almost the identity functor, but
re-abstracts its argument:
functor GenFun(N:NAT) = N :> NAT
156 / 248
Modules in other languages
Java also provides various structuring and hiding features:
I classes
I interfaces (with default implementations in Java 8)
I packages
I modules (from Java 9)
These offer independently developed, but overlapping,
facilities—beyond this current course.
157 / 248
Java 9 Modules – non-examinable summary
158 / 248
Java 9 Modules – non-examinable summary (2)
The module-info.java file names a module and specifies its
dependencies (on other modules) (requires keyword) and
public API (exports keyword) which exports packages for use
by other modules.
There’s lots of rich syntax for specifying access control, e.g.
I exports to exporting a package for use only within
another module.
I open for allowing reflective access. Modules by default
disallow this. (Prior to the addition of modules all Java
encapsulation could be circumvented by reflection!)
Remark: designing module systems (especially to fit in existing
systems) is hard. Project Jigsaw (Java’s module system) was
originally planned to be part of Java 7 (2011) but didn’t appear
until Java 9 (2017). There are whole books on the Java Module
System.
159 / 248
˜ Part C ˜
160 / 248
˜ Topic IX ˜
Languages for Concurrency and Parallelism
161 / 248
Sources of parallel computing
162 / 248
Language groups
163 / 248
Painful facts of parallel life
164 / 248
A programmer’s view of memory and timings
CPU - MEMORY
1 cycle
165 / 248
Multi-core-chip memory models
Today’s model (cache simplified to one level):
other CPU - FAST
2
or GPU etc MEMORY
6 DMA
?
incoherency
2
- CACHES
@ ?
CPU 1–15
1–15 200
@
coherency -6 R MEMORY
@
?
CPU 0 - CACHE 0 200
2
166 / 248
A Compute Cluster or Cloud-Computing Server
Multicore - MEMORY - NIC
CPU 999,999 999,999 999,999
6 6Ethernet
? ?
no connection Network e.g.
CPU 1 1 1
6 6Ethernet
? ?
no connection Network e.g.
CPU 0 0 0
167 / 248
Lecture focus: what programming abstractions?
168 / 248
What hardware architecture tells us
169 / 248
Communication abstractions for programming
170 / 248
Concurrent, Parallel, Distributed
171 / 248
SIMD, MIMD
172 / 248
Theoretical model – process algebra
173 / 248
Theoretical model – PRAM model
174 / 248
Oldest idea: Threads
175 / 248
Threads, and what’s wrong with them
176 / 248
Language support: Cilk
177 / 248
Language support: OpenMP
The directive “omp parallel for" tells the compiler “it is safe to do
the iterations in parallel".
Fortran “FORALL INDEPENDENT".
178 / 248
Threads and the like don’t work on clusters and clouds
179 / 248
Software support for message passing: MPI
180 / 248
Software support for message passing: Erlang
181 / 248
Cloud Computing
Can mean either any of:
I Doing one computer’s worth of work on a server instead of
locally. Google Docs.
I A glorified backed-up virtual disk or file server. Google
Drive, OneDrive, iCloud.
I Distributing computation across many remote computers,
and managing scheduling and dependency management
from within a programming framework:
Example frameworks: MapReduce (seminal), Spark,
Dryad, Naiad.
These have different levels of expressivity for different
algorithms (static vs dynamic data dependency graphs).
The first two are really only commercial special cases!
I Note the need for error resiliancy (errors happen often in
big computation), so need checkpoints or idempotent
(functional-like) computation.
Part II course: ‘Cloud Computing’.
182 / 248
Embarrassingly Parallel
183 / 248
Functional Programming
184 / 248
Garbage Collection
185 / 248
How have languages adapted to parallel hardware?
186 / 248
Partial language evolution: functional programming
ideas
187 / 248
Java 8: Internal vs. External iteration
Can’t trust users to iterate over data. Consider traditional
external iteration. It’s easy to start by writing
for (i : collection)
{ // whatever
}
188 / 248
Internal vs External iteration (2)
189 / 248
Java Collections vs Streams
Collections
I ‘Trusty old Java looping’; for-loops, internally using
mutable state (Iterators).
I Note that Java 8 did not add map and filter to the
Collections classes. Why?
Streams
I Almost ML-style understanding. Items need not be in
memory all at the same time (lazy).
I But additional laziness: a stream pipeline, e.g.
.filter().max() above, traverses the stream only once.
I .parallel() authorises parallel/arbitrary order of calls to
the stream pipeline. Good for parallelism, beware if you
use side-effecting functions on stream elements!
It’s quite practical to use .stream on a collection and then
process it as a stream and then convert back.
190 / 248
˜ Topic X ˜
Functional-style programming meets
object-orientation
191 / 248
Big picture
Historically, object-oriented and functional languages were
seen as disjoint programming paradigms.
en.wikipedia.org/wiki/Programming_paradigm
193 / 248
Java lambdas and closures
Single-argument lambdas in Java implement the Function
interface:
public interface Function<T,R> {
public <R> apply(T argument);
}
eval(Num(n)) = n
eval(Plus(t1,t2)) = eval(t1) + eval(t2)
eval(Times(t1,t2)) = eval(t1) * eval(t2)
prin(Num(n)) = string_of_int(n)
prin(Plus(t1,t2)) = "(" + prin(t1) + "+" + prin(t2) + ")"
prin(Times(t1,t2)) = "(" + prin(t1) + "*" + prin(t2) + ")"
196 / 248
Expr in OCaml
The above six cases are practically SML code already – we just
have to bundle the cases into two function definitions, each of
which considers three cases. (In OCaml we have to use
function or match for this.)
Additionally we need to define type Expr and three constructors
for expressions:
type Expr =
| Num of int
| Plus of Expr * Expr
| Times of Expr * Expr;;
197 / 248
Expr in Java
We define abstract class Expr subclassing it three times, each
subclass having two methods eval and prin. In principle just
(omitting some access qualifiers):
abstract class Expr { int eval(); String prin(); }
class Num extends Expr {
private int n; public Num(x) { n = x; }
int eval() { return n; }
String prin() { return Integer.toString(n); }
}
class Plus extends Expr {
private Expr t1, t2; public Plus(x1,x2) {t1=x1; t2=x2;}
int eval() { t1.eval() + t2.eval(); }
String prin() { return "(" + t1.prin() + "+" +
t2.prin() + ")"; }
}
class Times extends Expr {
private Expr t1, t2; public Times(x1,x2) {t1=x1; t2=x2;}
int eval() { return t1.eval() * t2.eval(); }
String prin() { return "(" + t1.prin() + "*" +
t2.prin() + ")"; }
} 198 / 248
Expr in Java (2)
199 / 248
Expr in OCaml vs. Java
Are there any differences, apart from the Java code feeling
more clumsy?
For a small fixed program, these appear to be simple
alternatives. But consider a larger program, possibly split over
multiple files and subject to ongoing maintenance
Adding a new form of data, e.g. Divide to Expr is:
I easy in Java, we just add a subclass to Expr
I hard in OCaml, we need to find every match or function
involving Expr.
Adding a new operation, e.g. compile to Expr is:
I hard in Java, every subclass of Expr needs a method
added
I easy in OCaml, just add a new top-level function.
200 / 248
Expr in OCaml vs. Java – the expression problem
The language-design problem of allowing a data-type definition
where one can add new cases to the datatype and new
functions over the datatype (without requiring ubiquitous
changes) is known as the “expression problem”:
en.wikipedia.org/wiki/Expression_problem
201 / 248
Value types
A value type is a type representing an a pure value – such
values can be copied, compared and subfields extracted but
subfields cannot be mutated. Complex numbers and immutable
arrays are examples, but mutable arrays are not. Strings of
course should be values, but C and C++ historically have had
issues as to whether they are char[] or const char[].
I In Java, every value is either of primitive type (spelt with
lower case) or is a reference to an object. Assignment ‘=’
copies bit patterns for primitives, but only copies
references for object types. Equality ‘==’ differs similarly.
I Worse, every reference-type object has identity, so that
new Complex(1.0,2.0) allocates a new distinguishable
object.
I This also explains why we are encouraged (given int i;
Integer x;) to write x = Integer.valueOf(i);
instead of x = new Integer(i);.
202 / 248
Value types and languages
When talking about value types, we want a type which only
admits functional update (copy but changing a field), not
mutability and no identity.
Why are value types useful? Increasing use of functional-style
APIs (partly due to the need to exploit parallelisms).
Think about defining a class Complex and note that every
arithmetic operation is likely to do an allocation. What we want
is the equivalent of struct { double re,im; } in C. C#
(Microsoft’s Java-style language) provides such values:
“Variables [of value type] directly contain values.
Assigning one value-type variable to another copies
the contained value. This differs from the assignment
of reference-type variables, which copies a reference
to the object but not the object itself.” [C# reference
manual]
203 / 248
Modelling value types in Java
We can model a value type in Java in two ways
I use final to stop modification; or
I use deep copy to snapshot values on operations.
Both of these are fragile during program enhancement.
For example a routine may copy an object to another object, but
program enhancement may change a value type into a
reference type, resulting in code elsewhere getting a value
which is part copied from, and part aliased with, a source
object.
I An object-copy routine which simply copies the fields of an
object is known as shallow copy
I An object-copy routine which recursively copies the fields
of an object is known as deep copy
204 / 248
Value types and inefficient representation
Think how we might represent an array of size 100 of complex
values in Java.
I We’ll have an array of 100 pointers (around 800 bytes) and
100 complex values (perhaps 24 bytes each).
I But at machine level we really just want to store 200
double values (only 1600 bytes).
This costs Java programs in memory use, cache effects and
instructions executed.
I Java would benefit from C#-like value types.
206 / 248
˜ Topic XI ˜
Miscellaneous (entertaining) concepts
I Monadic I/O
I Generalised Algebraic Data Types (GADTs)
I Continuation-passing style (CPS) and call/cc
Wikipedia:Call-with-current-continuation
I Dependent types (Coq, Agda, Idris)
207 / 248
I/O in functional languages
208 / 248
Giving different types to pure and impure functions
209 / 248
So, how does I/O work?
In ML we might read from and write to stdin/stdout with
(writing for emphasis):
MLrdint: unit int
MLwrint: int unit
210 / 248
Composing I/O functions
or Sequencing I/O effects
211 / 248
Digression – Monad Laws
212 / 248
Using the IO monad
213 / 248
Using the IO monad – examples
Read an integer, adds one to it, and print the result (using ML
syntax):
rdint >>= (fun x -> wrint(x+1));
Do this twice:
let doit = rdint >>= (fun x -> wrint(x+1))
in doit >>= (fun () -> doit);;
Note that doit has type unit IO, so we use >>= to use doit
twice.
NB: computations are not called as they are not functions; they
are combined using >>= as in the above examples.
214 / 248
Using the IO monad – examples (2)
215 / 248
Practical use
216 / 248
Other monads
I Many other unary type constructors have natural monadic
structure (at least as important as IO).
I For example, using Haskell syntax, List t and Maybe t.
Another important one is State s t of computations
returning a value of type t, but which can mutate a state of
type s. (Subtlety: the monad is legally the unary type
State s for some given s.)
I Haskell overloads >>= and return to work on all such
types (Haskell’s type class construct facilitates this).
I The common idea is ‘threading’ some idea of state
implicitly through a calculation.
Haskell example using List in GHCi:
[1,2,3] >>= \x->if x==2 then return 5 else [x,x]
[1,1,5,3,3]
217 / 248
Generalised Algebraic Data Types (GADTs)
218 / 248
How about this:
type _ exp = Val : ’a -> ’a exp
| Eq : ’a exp * ’a exp -> bool exp
| Add : int exp * int exp -> int exp
219 / 248
Can even write eval where the type of the result depends on
the value of its type:
let rec eval = function
| Val(x) -> x
| Eq(x,y) -> eval(x) = eval(y)
| Add(x,y) -> eval(x) + eval(y);;
220 / 248
Reified continuations
Or uncurrying
221 / 248
Instead of
let f(x) = ... return e ... ;;
print f(42);;
we write
let f’(k, x) = ... return (k e’) ... ;;
f’(print, 42);;
In CPS all functions return unit and all calls are now tail-calls
(so the above isn’t just a matter of adjusting a return statement).
Sussman and Steele papers from the 1970’s (“Lambda the
ultimate XXX”).
222 / 248
Reified continuations (2)
223 / 248
Reified continuations (3)
224 / 248
Dependent types
Suppose f is a curried function of n boolean arguments which
returns a boolean. How do we determine if f is a tautology
(always returns true)?
let ref taut(n,f) =
if n = 0 then f
else taut(n-1, f true) && taut(n-1, f false);
225 / 248
Dependent typing of taut in Idris
-- BF n is the type Bool -> ... -> Bool -> Bool
-- \_________________/
-- n
BF : Nat -> Type
BF Z = Bool
BF (S n) = Bool -> BF n
227 / 248
Approaches to storage allocation
I Manual, e.g. C/C++
Danger: user-incompetence (use after free)
I Automatic, e.g. Java, Python
Danger: unexpected delays (GC in a flight controller???)
I Ban it, MISRA (motor industry embedded coding standard)
Rule 18-4-1 “Dynamic heap memory allocation shall not be
used.”
I Type-managed – Rust
Upside: type system ensures memory- and thread-safety
and automatically adds calls to deallocate storage. No GC.
Downside: search online for “Rust hard to learn” – but this
is perhaps an advantage for smart programmers!
Fact: Rust is ranked 19 in the 2022 RedMonk language
rankings – so there are plenty of smart programmers and
interesting companies about!
228 / 248
Storage management is more than allocation and
deallocation
Regardless of manual or automatic deallocation there’s a wider
issue: ownership (related to the sweet spots on slides 222)
I If I pass a mutable object to be incorporated into a global
datastructure, then logically the call transfers ownership
from the caller to the callee, so I should never refer to it
again – just like free!
I When an API call returns me a record (which I plan to
mutate) is the caller or returner (callee) responsible for
copying it?
I Can we check this sort of property at compile time? Types?
Reasoning about ownership is more general than reasoning
about manual deallocation (freeing an object is just like passing
ownership to the pool of free memory).
229 / 248
Type Systems – weakness
In traditional type systems Γ ` e : t, variables have the same
type throughout the scope that introduce them.
This means that the three errors in the following program can’t
be detected by the type system:
230 / 248
Type Systems – weakness (2)
{ char *x = malloc(sizeof(SomeRecord));
x->field1 = 4; x->field2 = 5;
AddToGlobalDataStructure(x);
// x = malloc(sizeof(SomeRecord));
x->field1 = 8; x->field2 = 9;
AddToGlobalDataStructure(x);
// free(x);
}
232 / 248
Rust by example
(from the manual)
I Rust types look a bit like Java types with C-like qualifiers,
but be careful.
I Box<i32> is like ref in ML or a boxed int in Java:
fn create_box() {
let _box1 = Box::new(3i32); // ref to heap int
// ’_box1’ is destroyed (’dropped’) and its memory freed
// Resembles C++ RAII/destructors.
}
// destroy_box takes ownership of an item of
// heap-allocated memory (default call-by-value)
fn destroy_box(c: Box<i32>) {
println!("Destroying a box that contains {}", c);
// ’c’ goes out of scope here and is deallocated.
}
destroy_box(b);
println!("b contains: {}", b); // error, ’b’ not owner
}
234 / 248
Mutability
fn main() {
let immutable_box = Box::new(5u32);
println!("immutable_box contains {}", immutable_box);
*immutable_box = 4; // error
235 / 248
Borrowing
fn borrow_box(x: &Box<i32>) {
println!("A borrowed box (see below): {}={}" &x, x); }
fn main() {
let b = Box::new(5i32);
let c = Box::new(6i32);
borrow_box(&b); // 5=5
borrow_twice(&b,&c); // 5,6
borrow_twice(&b,&b); // 5,5
borrow_and_eat(&b,c); // 5,6
borrow_and_eat(&b,b); // error (borrow checker)
}
238 / 248
Exceptions: dynamic or static scoping?
I A mixture!
I Declaring an exception is statically scoped
exception Foo;
I Handling an exception is like dynamic scoping
exception Foo;
fun f():int = raise Foo;
fun g() = (try f() catch Foo => 1) + f();
fun main() = (try g() catch Foo => 42)
gives 42.
239 / 248
Resumable exceptions
I Resumable exceptions are generally called effects.
Why? Can see calls to side-effecting operations like IO as
having exceptional behaviour handled by OS (a system
call), and then your program is resumed.
I In general effects have result types to allow
resume-with-a-value (think read()).
I We now look at Koka programs using resumable
exceptions to model yield, dynamic scoping and Prolog
non-determinism.
I We use resume to return a value from an effect
I Koka subtlety: effects can be declared as ctl or fun.
Declaring an effect as fun is syntactic sugar – such code
desugars to use ctl and inserts resume automatically –
but also allows the compiler to generate more efficient
code.
240 / 248
A simple Koka program
// A generator effect with one ’fun’ operation
effect yieldeff
fun yield( x : int ) : ()
241 / 248
Using ctl exceptions
An optionally resumable (ctl) effect
effect yieldeff
ctl yield( x : int ) : ()
242 / 248
Effect names vs. effect-operation names
Minor naming subtlety
243 / 248
Dynamic scoping – using ctl effects
// Simulation of dynamic scoping
effect dyneff
ctl dynvar (s : string) : int
245 / 248
Prolog style backtracking as effects
fun myor(a: bool, b: bool) a&&b; // define non-short-circuit OR
effect choose
ctl flip() : bool // new: handlers resume flip() more than once
fun main()
satisfiable(mystery).println // True
246 / 248
The bigger picture
I We’ve focused on effects including exceptions (never
resume), fun effects (resume once), and Prolog-style
multiple resumptions.
I Effects are a structured use of continuations.
I Koka has a type system which models possible effects
(Haskell notion of ‘pure’ includes effects {div, exn}):
fun sqr : (int) -> total int // total: mathematical total function
fun divide : (int,int) -> exn int // exn: may raise an exception (partial)
fun turing : (tape) -> div int // div: may not terminate (diverge)
fun print : (string) -> console () // console: may write to the console
fun rand : () -> ndet int // ndet: non-deterministic
247 / 248
Places to look for more detail
I https://www.rust-lang.org/
I https://www.eff-lang.org/
I https://koka-lang.github.io/
248 / 248