Dynamic Types 15-312, Spring 2017 March 21, 2017 Announcements Homework 4 will be released shortly. Most of it is new, so it s hard to tell how hard we made it. Please start early! Look at what I made over Spring Break! https://github.com/jez/vim-better-sml New features: * Get the type of the variable under the cursor * Jump to the definition of a variable * Show type errors when they occur * Warn when a variable is unused Come to office hours with midterm questions. Questions What are the differences between dynamic typing and static typing? How do JIT compilers work? Dynamic Types & Static Types An important thing to keep in mind: Dynamic types are a mode of use of static types. When we talk about dynamic types, we re really talking about using a normal, static type system in a special way: with only one type. Every expression has this type. 1
We touched on this topic in lecture, but let s revisit it: what are the tradeoffs associated with dynamic types? Pro: Dynamic types have an easy safety proof. Con: The safety proof doesn t give you nearly as many guarantees. Pro: Con: Runtime tagging and untagging are slow, especially in loops. Pro: Omitting type annotations is makes certain code easier to write. Con: Type annotations are not possible, meaning they can t serve as documentation to readers. Pro: You can worry less about the types of arguments, because functions are generally liberal in what they accept. Con: Thinking about the types of expressions is a powerful programming tool for structuring code. Of course, there are other axes I ve omitted here. Feel free to consider more. Of these, I find that the Con s of dynamic typing outweigh the Pro s (and even that some of the Pro s are in fact non-pro s ). So the question still remains: why dynamic types at all? My guess is that it s easier to implement dynamic types than it is to implement ML modules. When you re designing a practical programming language, you want generic and polymorphism. The type-theoretic way to do this is to implement modules, which is hard. You also probably want to implement type inference on modules, which is harder 1. Alternatively, you can just give up and implement dynamic typing. 2 From a usability perspective, once you have type inference the code looks nearly identical in SML compared with, say, Python, so the conciseness of the code doesn t factor into the argument. It s rather that type inference like SML does it is actually non-trivial. The only remaining Pro is that people believe having to think about the types when writing the code is a hindrance, rather than a tool. This is a classic example of a tradeoff between a short term and long term gain: The initial cost to learning how types work, vs The longterm payout payout from using types to reason about your code. How highly do you value this tradeoff? 1 On the contrary, type inference for PCF (or similar languages) is rather straighforward. See for example https://github.com/jozefg/hm. 2 When designing languages, many people focus first on the concrete syntax of their language, rather than it s semantics. To see this, look at any canonical compilers textbook. The first subject covered is parsing, and usually so for disproportionately long. In this class, one of the main lessons is that language design should value the semantics above the syntax. 2
DPCF & HPCF In our discussions of dynamic types, we have two choices: treat all expressions as dyns treat dyn as yet another type that can be added to any other language DPCF is the former. It has the same computational power as PCF, but all expressions create dyns. Another language we can consider is HPCF. It is just PCF, but we add in a type dyn and operations on that type: τ ::= dyn d ::= x l ::= num fun τ ::= nat τ τ dyn e ::= x z z s d s e ifz { z d s x d ifz { z d s x d λx. d λx. e d d e e fix x is d fix x is e num[n] l! e tag e @ l cast l? e is-a? Optimizing Dynamic Code Let s consider the plus function in DPCF. // Note how DPCF requires no type annotations... // everything's a dyn! fix plus is λn. λm. If we were to dynamically evaluate this function, we would see a lot of runtime checks for things like is_num and is_fun. These checks are compounded by the fact that we have to run them on every recursive call, oftentimes for things which we already know the types of. 3
Let s optimize this optimize this function this by translating it to HPCF, and then moving around some of the checks that we know are no-ops. // In HPCF, we have to annotate types, and we have // to introduce dyns with! and eliminate them with @ fix plus : dyn is fun! λn : dyn. fun! λm : dyn. ifz (n @ num) { s n' num! (s ((((plus @ fun) (num! n'))@fun m) @ num)) But we know that plus is actually always a dyn -> dyn -> dyn. Let s rewrite our code to make use of this fact: fix plus : dyn -> dyn -> dyn is λn : dyn. λm : dyn. ifz (n @ num) { s n' num! (s (plus (num! n') m) @ num) But wait, we know that every invocation of plus recursively will essentially use it like nat -> nat -> nat. Let s make that optimization: fix plus : nat -> nat -> nat is λn : nat. λm : nat. Hmm This looks suspiciously like normal PCF code. We don t use any of our new HPCF constructs here! However, we ve changed the type. External call sites expect that plus is a dyn, so let s wrap it back up as such: let val fastplus = in end fix plus : nat -> nat -> nat is λn : nat. λm : nat. fun! λx : dyn. fun! λy : dyn. num! fastplus (x @ num) (y @ num) 4
And voilà. So in the process of optimizing the code, we really just re-wrote it with types. This allowed us to get rid of the excessive tagging and untagging operations, so our inner loop ran really fast. This is how JIT compilers work. They start out by interpretting your code, but then after a while make note when a function is being called with only certain types of arguments. When this happens, they compile the function in question to native (i.e., typed) code, and dispatch to the compiled code, instead of the interpreted code. To learn more: https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/ 5