The Behavior of Gradual Types: a User Study
Total Page:16
File Type:pdf, Size:1020Kb
The Behavior of Gradual Types: A User Study Preston Tunnell Wilson Ben Greenman Brown University Northeastern University Providence, Rhode Island Boston, Massachusetts [email protected] [email protected] Justin Pombrio Shriram Krishnamurthi Brown University Brown University Providence, Rhode Island Providence, Rhode Island [email protected] [email protected] Abstract But what should the semantics of a gradually-typed pro- There are several different gradual typing semantics, reflect- gram be? In particular, when typed and untyped regions of ing different trade-offs between performance and type sound- code interact, what sort of runtime checks should protect ness guarantees. Notably absent, however, are any data on the invariants of typed code? The answer to this question which of these semantics developers actually prefer. has implications for soundness, simplicity, performance, and We begin to rectify this shortcoming by surveying pro- (for retrofitted type systems) backward compatibility. fessional developers, computer science students, and Me- Several papers presenting these systems justify their de- chanical Turk workers on their preferences between three signs by appealing to what they consider natural or intuitive gradual typing semantics. These semantics reflect important to programmers [8, 9, 28, 33, 37]. However, none of the papers points in the design space, corresponding to the behaviors of provide evidence to justify those claims. Our work repairs Typed Racket, TypeScript, and Reticulated Python. Our most this weakness by performing the first study of developer important finding is that our respondents prefer a runtime preferences between different gradual typing semantics. semantics that fully enforces statically declared types. Concretely, we focus on three semantics: those corre- sponding to Typed Racket (Deep), TypeScript (Erasure), and ACM Reference Format: Reticulated Python (Shallow). We adapt these semantics Preston Tunnell Wilson, Ben Greenman, Justin Pombrio, and Shri- to a common surface language in the manner suggested by ram Krishnamurthi. 2018. The Behavior of Gradual Types: A User Greenman and Felleisen [12] and thus obtain three possibly- Study. In Proceedings of Dynamic Languages Symposium (DLS’18). distinct behaviors for one mixed-typed program. Deep treats ACM, New York, NY, USA, 12 pages. https://doi.org/10.1145/nnnnnnn. nnnnnnn type annotations as (higher-order) contracts between regions of code. Erasure ignores type annotations at runtime. Shal- low lies between these extremes; it checks values against 1 Introduction type constructors. In recent years, the long-standing debate between static and We design a survey to illustrate key differences between dynamic typing has been finding a reconciliation: gradual the three semantics using short programs. We administer the typing [27, 31]. In a gradually typed language, program- survey to three populations: developers at a major software mers are free to mix typed and untyped code. Some of the company, computer science students, and Mechanical Turk early gradually typed languages were created by retrofitting workers. We find that respondents generally dislike Erasure a type system on a (sublanguage of a) dynamic language and like behavior that aligns with a statically-typed language. (e.g., Typed Racket [32, 33], TypeScript [3], and Reticulated In addition to our findings, the survey is itself useful as acol- Python [36]); more recently, new languages are being made lection of representative programs that a language definition gradually typed from the outset, such as Pyret (pyret.org) can use in its manuals to explain its runtime behavior. and Dart 1 (v1-dartlang-org.firebaseapp.com). Permission to make digital or hard copies of part or all of this work for 2 Three Approaches to Gradual Typing personal or classroom use is granted without fee provided that copies are Soundness is a desirable property for any type system be- not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for third- cause it relates the ahead-of-time claims of the types to run- party components of this work must be honored. For all other uses, contact time outcomes. For example, if a sound type system claims the owner/author(s). that an expression e is of type Int×Int (representing a tuple DLS’18, November 2018, Boston, Massachusetts USA of integers) and the evaluation of e yields a value, then the © 2018 Copyright held by the owner/author(s). value is definitely a tuple with integer components. This fact ACM ISBN 978-x-xxxx-xxxx-x/YY/MM. about the tuple e can be used to state similar guarantees https://doi.org/10.1145/nnnnnnn.nnnnnnn 1 DLS’18, November 2018, Boston, Massachusetts USA Tunnell Wilson et al. about code that interacts with the tuple, and in general a pro- To this end, Deep strictly enforces the source-code bound- grammer can use type soundness to reason compositionally aries between statically-typed and dynamically-typed code. about the correctness of a program. If a typed context imports an untyped value, the value goes A gradual typing system, however, cannot be sound in through a structural check. Dually, if an untyped context the normal sense because such systems let untyped values imports a typed function, the function receives latent pro- interact with statically-typed regions of code. For example, tection against untyped inputs in the form of a derived type a typed module that imports an untyped value must declare boundary. Runtime checks occur only at source-code bound- a static type for the value, but cannot know until runtime aries and at derived boundaries for higher-order values. whether the value matches the type. To illustrate, the typed When an untyped value v flows into a context that expects code below expects a tuple of numbers but receives a tuple some value of type τ , written v =) (· : τ ), Deep employs of strings at runtime: the following type-directed validation strategy: 1 // UNTYPED code Deep Strategy 2 var f = function(x) { return (x, x); } • v =) (· : Int) 3 check that v is an integer 4 // TYPED code • v =) (· : τ0 ×τ1) 5 declare function check that v is a tuple and recursively check its com- 6 f(x: String) : (Number, Number); ponents; in particular, check that v = hv0;v1i and 7 recursively check vi =) (· : τi ) for each element 8 var nums : (Number, Number) = • v =) (· : τd !τc ) 9 f(" NaN "); check that v is a function and wrap v in a proxy that 10 var num : Number = protects future inputs and checks future outputs (see 11 nums [0]; Matthews and Findler [18] §3 for a discussion). The question for gradual typing is: how to defend a statically- In summary, the Deep strategy eagerly checks finite values typed context against a mismatched untyped value? and lazily checks infinite values. Three strategies have emerged: Deep, Erasure, and Shal- low. In terms of the typed code above, which is internally 2.2 Erasure: Ignore Types type-correct, Erasure runs the program to completion de- The Erasure strategy uses types for static analysis, and spite the mismatch. Deep inserts an assertion that the call to nothing more. At runtime, any value may flow into any f on line 9 returns a tuple of numbers and, because the tu- context regardless of the type annotations: ple contains strings, halts before completing the assignment Erasure Strategy on line 8. Shallow only asserts that the call to f returns • ) · a tuple; because f does, this check passes. It later asserts v = ( : Int) that nums[0] on line 11 returns a number. The latter check check nothing • ) · × fails and Shallow halts before the assignment on line 10. v = ( : τ0 τ1) Generally, at runtime, Deep enforces types, Erasure ignores check nothing • ) · ! types, and Shallow enforces type constructors. v = ( : τd τc ) The following subsections outline the three strategies in check nothing more detail by explaining: (1) the motivation, (2) the source- Despite the complete lack of type soundness, the Erasure code positions where runtime checks may occur, and (3) strategy is popular among implementations of gradual typ- the nature of the runtime checks. Since each runtime check ing. For one, the static type checker can point out logical corresponds to a type, the examples assume a base type errors in type-annotated code. Second, an IDE may use the representing the set of integer values, an inductive type static types in auto-completion and refactoring tools. Third, representing tuples, and a coinductive type for functions: Erasure is simpler to implement than any form of type τ = Int j τ ×τ j τ !τ enforcement. Fourth, users that are familiar with the host The reader may extrapolate an enforcement strategy for language do not need to learn a new semantics to understand other base types (e.g., strings), inductive types (e.g., im- the behavior of type-annotated programs. Fifth, Erasure mutable sets), and coinductive types (e.g. arrays, objects). runs as fast as the original language. 2.1 Deep: Enforce Types 2.3 Shallow: Protect Typed Code The goal of the Deep strategy is to offer a generalized notion The Shallow strategy ensures that typed code does not “go of type soundness. Interactions between typed and untyped wrong” [22] in the sense of applying a primitive operation to code may lead to a mismatch at runtime, but otherwise the a value outside its domain. For example, Shallow ensures programmer can trust the static types. that every function call targets a callable value. 2 The Behavior of