Lecture 24
Static vs. Dynamic Typing
Programming Languages
UW CSE 341 - Autumn 2022
Updates
OCaml vs. Racket
Much to discuss!
OCaml from Racket Perspective
Racket from OCaml Perspective
let car v = match v with Pair(a,_) -> a | _ -> failwith …
let pair? v = Bool(match v with Pair _ -> true | _ -> false)
Static Checking
Languages
Example: what OCaml’s types prevent
Example: what OCaml’s types allow
Types are designed to “prevent bad things”
A question of eagerness
“Catching a bug before it matters”
is in inherent tension with
“Don’t report a bug that might not matter”
Static checking / dynamic checking are two points on a continuum
Silly example: Suppose we just want to prevent evaluating 3 / 0
Correctness
Suppose a type system is supposed to prevent X for some X
The goal is usually for a PL type system to be sound (so you can rely on it) but not complete
Notice soundness/completeness is with respect to X
Venn Diagrams
All possible programs
(in syntax of some language)
Venn Diagrams
Programs for which there is >= 1 input that makes the program do bad thing X
All possible programs
(in syntax of some language)
Programs for which there is no input that makes the program do bad thing X
Venn Diagrams
Programs for which there is >= 1 input that makes the program do bad thing X
All possible programs
(in syntax of some language)
Programs for which there is no input that makes the program do bad thing X
Type system accepting these programs is sound but incomplete (common goal)
Venn Diagrams
Programs for which there is >= 1 input that makes the program do bad thing X
All possible programs
(in syntax of some language)
Programs for which there is no input that makes the program do bad thing X
Type system accepting these programs is unsound and incomplete
Venn Diagrams
Programs for which there is >= 1 input that makes the program do bad thing X
All possible programs
(in syntax of some language)
Programs for which there is no input that makes the program do bad thing X
Type system accepting these programs is unsound and is complete
Venn Diagrams
Programs for which there is >= 1 input that makes the program do bad thing X
All possible programs
(in syntax of some language)
Programs for which there is no input that makes the program do bad thing X
Type system accepting these programs is sound and complete (impossible if type-checker always terminates)
Incompleteness
OCaml rejects these even though they never misuse division:
let f1 x = 4 / "hi" (* but f1 never called *)
let f2 x = if true then 0 else 4 / "hi"
let f3 x = if x then 0 else 4 / "hi"
let x = f3 true
let f4 x = if x <= abs x then 0 else 4 / "hi"
fun f5 x = 4 / x
let y = f5 (if true then 1 else "hi")
Why incompleteness
Undecidability
Undecidability is an essential concept at the core of computing
Most common example: The Halting Problem
What about unsoundness?
Suppose a type system were unsound. What could the PL do?
Why weak typing (C/C++)
Weak typing: There exist programs that, by definition, must pass static checking but then when run can “set the computer on fire”?
Weak typing is a poor name: Really about doing neither static nor dynamic checks
What weak typing has caused
Example: Racket
This is nothing like the “catch-fire semantics” of weak typing
Another misconception
What operations are primitives defined on and when an error?
This is not static vs. dynamic checking (sometimes confused with it)
Now can argue…
Having carefully stated facts about static checking, can now consider arguments about which is better:
static checking or dynamic checking
Remember most languages do some of each
Claim 1a: Dynamic is more convenient
Dynamic typing lets you build a heterogeneous list or return a “number or a string” without workarounds
(define (f y)
(if (> y 0) (+ y y) "hi"))
(let ([ans (f x)])
(if (number? ans) (number->string ans) ans))
type t = Int of int | String of string
let f y = if y > 0 then Int(y+y) else String "hi"
match f x with
| Int i -> string_of_int i
| String s -> s
Claim 1b: Static is more convenient
Can assume data has the expected type without cluttering code with dynamic checks or having errors far from the logical mistake
(define (cube x)
(if (not (number? x))
(error "bad arguments")
(* x x x)))
(cube 7)
let cube x = x * x * x
cube 7
Claim 2a: Static prevents useful programs
Any sound static type system forbids programs that do nothing wrong, forcing programmers to code around limitations
let f g = (g 7, g true) (* does not type-check *)
let pair_of_pairs = f (fun x -> (x, x))
(define (f g)
(cons (g 7) (g #t)))
(define pair_of_pairs
(f (lambda (x) (cons x x))))
Claim 2b: Static lets you tag as needed
Rather than suffer time, space, and late-errors costs of tagging everything, statically typed languages let programmers “tag as needed” (e.g., with variants)
In the extreme, always possible to do enough tagging
Claim 3a: Static catches bugs earlier
Static typing catches many simple bugs as soon as “compiled”
(define (pow x) ; curried
(lambda (y)
(if (= y 0)
1
(* x (pow x (- y 1)))))) ; oops
let rec pow x y = (* does not type-check *)
if y = 0
then 1
else x * pow (x,y-1)
Claim 3b: Static catches only easy bugs
But static often catches only “easy” bugs, so you still have to test your functions, which should find the “easy” bugs too
(define (pow x) ; curried
(lambda (y)
(if (= y 0)
1
(+ x ((pow x) (- y 1)))))) ; oops
let rec pow x y = (* curried *)
if y = 0
then 1
else x + pow x (y-1) (* oops *)
Claim 4a: Static typing is faster
Language implementation:
Your code:
Claim 4b: Dynamic typing is faster
Language implementation:
Your code:
Claim 5a: Code reuse easier with dynamic
Without a restrictive type system, more code can just be reused with data of different types
Claim 5b: Code reuse easier with static
So far
Considered 5 things important when writing code:
But took the naive view that software is developed by taking an existing spec, coding it up, testing it, and declaring victory.
Reality:
Claim 6a: Dynamic better for prototyping
Early on, you may not know what cases you need in variants and functions
Claim 6b: Static better for prototyping
What better way to document your evolving decisions on data structures and code-cases than with the type system?
Easy to put in temporary stubs as necessary, such as
| _ -> raise Unimplemented
Claim 7a: Dynamic better for evolution
Can change code to be more permissive without affecting old callers
(define (f x) (* 2 x))
(define (f x)
(if (number? x)
(* 2 x)
(string-append x x)))
let f x = 2 * x
let f x =
match x with
Int i -> Int (2 * i)
| String s -> String(s ^ s)
Claim 7b: Static better for evolution
When we change type of data or code, the type-checker gives us a “to do” list of everything that must change
Example: Changing the return type of a function
Example: Adding a new constructor to a variant
Counter-argument: The to-do list is mandatory, which makes evolution in pieces a pain: cannot test part-way through
Coda