Putting gradual types to work
PPutting gradual types to work
Bhargav Shivkumar , − − − , Enrique Naudon − − − ,and Lukasz Ziarek Bloomberg, New York, USA SUNY - University at Buffalo , New York, USA
Abstract.
In this paper, we describe our experience incorporating gradual typesin a statically typed functional language with Hindley-Milner style type infer-ence. Where most gradually typed systems aim to improve static checking in adynamically typed language, we approach it from the opposite perspective andpromote dynamic checking in a statically typed language. Our approach providesa glimpse into how languages like SML and OCaml might handle gradual typing.We discuss our implementation and challenges faced—specifically how gradualtyping rules apply to our representation of composite and recursive types. Wereview the various implementations that add dynamic typing to a statically typedlanguage in order to highlight the different ways of mixing static and dynamictyping and examine possible inspirations while maintaining the gradual nature ofour type system. This paper also discusses our motivation for adding gradual typesto our language, and the practical benefits of doing so in our industrial setting.
Keywords:
Gradual typing · Type inference · Functional programming
Static typing and dynamic typing are two opposing type system paradigms. Staticallytyped languages are able to catch more programmer bugs early in the compilation process,at the expense of a more flexible semantics. On the other hand, dynamically typedlanguages allow greater flexibility, while allowing more bugs at runtime. The proponentsof each paradigm often feel very strongly in favor of their paradigm. Language designersare stranded in the middle of this dichotomy and left to decide between the two extremeswhen designing their languages.At Bloomberg, we have felt this pain while designing a domain specific languagefor programmatically defining financial contracts. For the purposes of this paper, wewill call our language Bloomberg Contract Language (BCL). BCL is a statically typedfunctional language with Hindley-Milner style type inference [4,17], structural compositetypes and recursive types. Users of BCL are split into two groups—end users andlanguage maintainers. End users are typically financial professionals whose primaryprogramming experience involves scripting in dynamically typed languages such asPython and MATLAB. On the other hand, language maintainers are Bloomberg softwareengineers who are most at ease programming in statically typed and often functionallanguages like OCaml. Whilst it is of paramount importance to provide our end userswith an environment in which they are comfortable, our domain—financial contracts—is one in which correctness is of extraordinary importance, since errors can lead to a r X i v : . [ c s . P L ] J a n Shivkumar et al. large financial losses. This makes static types appealing, as they catch many errors thatdynamic systems might miss. Even though static types provide a more error-free runtime,they do require extra effort from our end users who must learn an unfamiliar system.Our desire to simultaneously satisfy our end users and our language maintainers led usto gradual typing [23], which seeks to integrate static and dynamic typing in one system.Gradual typing in BCL allows language maintainers to stick to static typing and endusers to selectively disable static typing when it interferes with their ability to work inBCL.Since its introduction, gradual typing [23] has been making its way into moremainstream languages [30,29] and more people have acknowledged the varied benefitsof mixing static and dynamic typing in the same program. As identified by Siek andTaha [26], there has been considerable interest in integrating static and dynamic typing,both in academia and in industry. There has also been a plethora of proposed approaches,from adding a dynamic keyword [2], to using objects in object-oriented languages [16],to Seik and Taha’s gradual typing itself [23]. While there seems to be no one-size-fits-all approach to designing a system that mixes static and dynamic types, Siek andTaha standardize the guarantees [26] we can expect from such a system. For languagedesigners, this provides a more methodical way to approach the integration. Languagedesigners can also draw from a large body of literature exploring the combination ofgradual types with other common features, such as objects [24] and type inference [25,5].While it is typical for dynamically typed languages to go the gradual route in orderto incorporate more static type checking, we go the other way and add more dynamismto our already static language. Most static languages that incorporate dynamic typing doso by following in the footsteps of Abadi et.al. [2]–C ? annotationto explicitly signify dynamically typed terms while un-annotated terms are (implicitly)statically typed, much like that of Garcia and Cimini [5]. This approach provides asimple escape hatch to end users who want to use dynamic typing as well as avenues toautomate this process to ensure backwards compatibility of BCL with legacy code.Finally, we feel there is a need to study the adaptation of gradual types to an existinglanguage with a substantial user base and lots of existing code. We aim to provide atechnical report in this paper that models our design decisions and implementation detailsof bringing in gradual types to BCL. Our primary contributions include: – A brief review of other statically typed languages that add dynamic types, to compareand possibly derive inspiration for our own design in Section 2. – Introduce a new use case that shows how a gradually typed language benefitsdifferent user groups of a language in Section 3. – An inference algorithm, which is an adaptation of a prominent inference algorithmto add gradual types to a language with type inference in Section 4.Note that throughout this paper we use "gradual" to indicate an implementation thatprovides gradual guarantees as specified in [26]. While, we do not state this formally forBCL and leave that to future work, our implementation supports the smooth evolution ofprograms from static to dynamic typing as prescribed for gradually typed systems. utting gradual types to work 3
In this section we briefly survey the existing literature to better contextualize our de-sign choices. The incorporation of static and dynamic typing has been extensivelystudied [28,15,23,26,8], though usually in the context of a core calculus instead of afull-featured language. There also seems to be a juxtaposition of the literature, whichgenerally follows a static-first approach, and practical implementations, which generallyfollow a dynamic-first approach [7].Abadi et al [2] has been an inspiration for many static languages looking to incorpo-rate dynamic typing. This work is a precursor to gradual typing, and while it does notqualify as gradual à la [23], it is nevertheless a standard when it comes to adding dynamicchecks to a static language. Abadi’s work uses a dynamic construct to build terms oftype Dynamic and a typecase construct to perform case analysis on the runtimetype of an expression of type
Dynamic . This is similar to the typeof() function indynamic languages like Python, which resolve the type of an expression at runtime. Siekand Taha observe that translating from their language of explicit casts to Abadi et al’slanguage is not straightforward [23]. Nevertheless we believe that it is worthwhile tointroduce something like the typecase construct in a static language with gradualtypes. We identify and discuss some potential applications of this in Section 5.Statically typed object oriented languages like C dynamic type todeclare objects that can bypass static type checking [1]. Although this achieves dynamictype checking, there is no indication of it being gradual à la [23]. Moreover, using the dynamic type in a C Here, static-first refers elaborating a static surface language to a gradually typed intermediaterepresentation. Conversely, by dynamic-first we mean the opposite: elaborating a dynamicsurface language to a gradually typed intermediate representation. Shivkumar et al. (SVAR) Γ ( x ) = τS ; Γ (cid:96) x : τ S ; Γ (cid:96) e : τ (SCNST) S ; Γ (cid:96) c : typeof ( c ) (SAPP) S ; Γ (cid:96) e : τ S ; Γ (cid:96) e : τ S ( τ ) = S ( τ → τ ) S ; Γ (cid:96) e e : τ (SABS) S ; Γ ( x (cid:55)→ τ ) (cid:96) e : τ S ; Γ (cid:96) λx : τ .e : τ → τ (a) λ α → (GVAR) Γ ( x ) = τS ; Γ (cid:96) g x : τ S ; Γ (cid:96) g e : τ (GCNST) S ; Γ (cid:96) g c : typeof ( c ) (GAPP) S ; Γ (cid:96) g e : τ S ; Γ (cid:96) g e : τ S | = τ (cid:39) τ → β ( βfresh ) S ; Γ (cid:96) g e e : β (GABS) S ; Γ ( x (cid:55)→ τ ) (cid:96) g e : τ S ; Γ (cid:96) g λx : τ .e : τ → τ (b) λ ? α → Fig. 1: Simply and gradually typed lambda calculus with type variablesFig. 2: Huet’s unification of { α → α = Int → β } Siek and Vachchrajani [25](S&V) propose an innovative solution for performinggradual type inference which combines gradual typing with type inference. Their maingoal is to allow inference to operate on the statically typed parts of the code, whileleaving the dynamic parts to runtime checks. Furthermore, the dynamic type must unifywith static types and type variables, so that the static and dynamic portions of code mayfreely interact. In this section, we summarize their work.The work of S&V is based on the gradually typed lambda calculus [23]. The graduallytyped lambda calculus extends the simply typed lambda calculus ( λ → ) with an unknowntype, ? –pronounced “dynamic”; type checking for terms of this type is left until runtime.The gradually typed lambda calculus ( λ ? → ) allows static and dynamic types to freely mixand satisfies the gradual guarantee [26], ensuring smooth migration between static anddynamic code while maintaining the correctness of the program.Type inconsistencies in λ ? → are caught by a consistent relation, instead of equality asin λ → . The consistent relation only compares parts of a type that are statically known; itis one of the key contributions of λ ? → . All type errors that cannot be statically resolvedby the gradual type system are delegated to runtime checks.Type inference allows programmers to omit type annotations in their programs andhave the compiler infer the types for them. Hindley-Milner type inference is often cast utting gradual types to work 5 as a two step process that consists of generating constraints and then solving them bya unification algorithm [32,20,21]. The inference algorithm models the typing rules asequations, called constraints, between type variables, while the unification algorithmcomputes a substitution S , which is a mapping from type variables to types, such thatfor each equation τ = τ , we have S ( τ ) = S ( τ ) .S&V introduce the gradually typed lambda calculus with type variables ( λ ? α → ), whichis λ ? → extended with type variables, α . They define a new relation, consistent-equal ( (cid:39) ),which extends the consistent relation from λ ? → to treatment α . Fig. 1b compares thetyping rules for λ α → , the statically typed lambda calculus with type variables, to the newtype system λ ? α → . S&V also specify a unification algorithm for λ ? α → which integrates the consistent-equal into Huet’s unification algorithm [10,14] which is a popular algorithmthat doesn’t rely on substitution.Huet’s unification algorithm uses a graph representation for types. For example, atype like Int → β is represented as a sub graph in Fig. 2. A node represents a type,ground types, type variables or the function type ( → ), and edges connect the nodesof types belonging to a → type. From this it follows that the unification algorithm isthe amalgamation of two graphs present in a constraint equation following the rulesof the type system. Huet’s algorithm maintains a union find structure [27] to maintainequivalence classes among nodes and thereby types. When node A unifies with node B according to the type rules, the merge results in one of the two nodes becoming therepresentative of the merge. This signifies that the representative node is the solutionto the constraint being unified. Fig. 2 shows how the unification of the constraint { α → α = Int → β } proceeds. Our motivation to explore gradual types for BCL is rooted in several historical andcontextual details, which we discuss in this section. It is first helpful to understand thatBCL is predominantly used to model financial contracts, by providing end users withprogrammatic access to a financial contract library. The library we use is based uponthe composable contracts of Peyton Jones, Eber and Seward [19]. Its internal contractdata structure is used throughout our broader derivatives system to support variousdownstream analyses. In this way, BCL serves as an expressive front-end for describingcontracts to our derivatives system.Let us look at a short illustrative example of BCL code. Fig. 3 provides an exampleof the sort of thing for which BCL might be used. The european_stock_option function produces a
Contract which models a European stock option. Europeanstock options grant their holder the right, but not the obligation, to buy or sell stock ina company. The “European” in European stock option refers to the fact that, on onespecific date, the holder must choose whether or not s/he would like to buy (or sell) thestock. This is in contrast to “American” options, where the holder may choose to buy (orsell) on any date within a specified range of dates.This stock option is based on several helper functions, defined in [19], which we mustexamine first. The european function constructs a contract which allows its holderto choose between receiving “something” or nothing on a specified date. receive
Shivkumar et al. let receive currency amount = scale (one currency) amount inlet european_stock_option args = let first = stock_price args.effective_date args.company inlet last = stock_price args.expiry_date args.company inlet payoff = match args.call_or_put with | Call -> (last / first - args.strike)| Put -> (args.strike - last / first) in european args.expiry_date (receive args.currency payoff) in european_stock_option{ company = "ABC Co.",call_or_put = Call,strike = 100.0,currency = USD,effective_date = 2021-01-17,expiry_date = 2021-01-22 } Fig. 3: European stock optionconstructs a contract that pays the specified amount of the specified currency passedas arguments andn uses the scale and one primitives. The scale primitive takesan amount of type
Obs Double –where type
Obs d represents a time-varying quantityof type d –and a contract as arguments and multiplies key values in the contract bythe amount. Note that european_stock_option uses - and / operators whichare built-ins that operate on Obs Double arguments. stock_price is a primitive forlooking up the price of the specified stock on the specified date. european_stock_option starts off by using stock_price to look up theprice of the specified company’s stock on the “effective” (contract start) and “expiry”(contract end) dates. It uses these stock prices to construct the payoff based on thespecified call or put style, and feeds the payoff to receive to construct a contract thatpays it. Finally european_stock_option passes the result of receive to the european , which allows the holder to choose between the payoff and nothing. Notethat the payoff may well be negative, so the holder’s choice is not entirely clear. The endof Fig. 3, provides an example call european_stock_option which constructsa call option on ABC Co. In practice, functions like european_stock_option would be defined in BCL’s standard library, and would be called by users who wish tomodel European stock options directly or who wish to model contracts that contain suchoptions as sub-contracts. utting gradual types to work 7
Given that BCL is mostly used to describe financial contracts, it should come as nosurprise that our users are largely financial professionals. In particular, many are financialengineers or quantitative analysts with some programming experience in dynamic lan-guages such as Python and MATLAB. Typically these users need to translate term sheets,plain-English descriptions of a contract, into BCL for consumption by our system. Thesecontracts are mostly one-off and, once finished, are unlikely to be reused as subcontractsto build further contracts. For these reasons, the users of BCL are primarily concernedwith development speed. Ideally, they would like to be able to translate a term sheet asquickly as possible, so that they may focus on analyzing the contract’s behavior once ithas been ingested by our system.On the other hand, the maintainers of BCL and its standard library are softwareengineers and functional programmers with extensive experience in OCaml, C ++ andother static languages. The main jobs of the BCL maintainers are implementing languageextensions and standard library functions. One of the significant constraints that they faceis preserving backwards compatibility. All existing user contracts must continue to workas BCL evolves–even minor changes in behavior are unacceptable! Given the broad reuseof the features that BCL’s language maintainers implement and the difficulties involvedin rolling back features, correctness is the paramount concern of BCL maintainers.Finally, it is important to note that the version of BCL described here is actually thesecond version of BCL. The first version of BCL was dynamically typed, so we willdistinguish it from the second version by referring to it as Dynamic BCL. DynamicBCL supports only a few primitive data types, as well as a list composite type; it doesnot support algebraic types. It also runs only minimal validation before attemptingevaluation. This simplicity makes Dynamic BCL well suited to our users who seekto quickly feed contracts into our system, but ill-suited to the library code writtenby our maintainers. Additionally, some users who encounter runtime type errors whileimplementing particularly complex contracts would turn to the maintainers for assistance,further increasing the burden on the maintainers. It was in light of these issues, that wedeveloped (Static) BCL.To address the issues with Dynamic BCL while remaining useful to our users, BCLaims to be a static language that feels roughly dynamic. To this end, BCL supportsimplicit static types via type inference; we chose Hindley-Milner style inference so thatour users could omit type annotations in almost all cases. BCL also supports record andvariant types, although they are structural rather than the nominal ones typically seen inOCaml and Haskell. This choice also lends BCL a more dynamic feel.The goal of BCL’s design is to retain enough flexibility for our users, while introduc-ing static types for the benefit of our language maintainers. However, “enough flexibility”is entirely subjective and some users may well feel that any amount of static checkingresults in a system that is too inflexible. Gradual types address this concern by allowingusers to use dynamic types where they like, while also allowing maintainers to use statictypes where they would like. Importantly, gradual types guarantee that fully dynamiccode and fully static code can co-exist, and that static code is never blamed for runtimetype errors. Taken together, these two guarantees satisfy both groups, and ensure that thetype errors that dynamic users see are isolated to the code that they themselves wrote. Shivkumar et al.
BCL’s core calculus is the lambda calculus extended with structural composite typesand recursive types. Furthermore, BCL is implicitly-typed and supports Hindley-Milnerstyle type inference. This section describes the types and terms of this core calculus.Note, however, that the grammars in this section are abstract representations of BCL’stheoretical underpinnings, and do not cover the full set of productions in BCL’s grammar. κ ::= ∗ | ρ | κ ⇒ κ C ::= →| Π | Σ | ...τ ::= α | C | τ τ | l : τ ; τ | (cid:15) | µα.τ σ ::= τ | ∀ α.σ Fig. 4: Grammar of types and kinds
Kinds and Types
The grammar of the types and kinds that describe BCL is given inFig. 4. Our kind system is fairly standard and consists of only three forms. The base kind, ∗ , is the kind of “proper” types– Int and
Int → Int , for example–which themselvesdescribe terms. The row kind, ρ , is of course the kind for rows. The operator kind, ⇒ , isthe kind of type operators – Array and → , for example – which take types as argumentsand which do not directly describe terms. C ranges over type constructors, including the type operators for function types ( → of kind ∗ ⇒ ∗ ⇒ ∗ ), record types ( Π of kind ρ ⇒ ∗ ) and variant types ( Σ of kind ρ ⇒ ∗ ). C may also include additional constructors for base types (e.g. Int and
String )and more type operators (e.g.
Array ) as desired. However, these additional constructorsare not useful for our purposes here, so we make no further mention of them.Our type system is stratified into monomorphic types and type schemes, per [4].Monomorphic types, τ , consist of type variables, type constructors, and record, variantand recursive types. Type variables are ranged over by α , β , γ , etc., and are explicitlybound by µ and ∀ types, as described below. Rows are written l : τ ; τ (cid:48) , indicating thatthe row has a field labeled l of type τ . τ (cid:48) has kind ρ and dictates the other fields that therow may contain. If τ (cid:48) is a type variable, the row can contain arbitrary additional fields;if τ (cid:48) is the empty row, (cid:15) , the row contains no additional fields; finally if τ (cid:48) is another typeof the form l : τ ; τ (cid:48) , then the row contains exactly the fields specified therein. Recursivetypes are written µα.τ , where the variable α represents the point of recursion and isbound within τ . BCL’s recursive types are equi-recursive, so it does not have explicitconstructs for rolling and unrolling recursive types. Finally, type schemes have twoforms: monomorphic types and universally quantified schemes. Monomorphic types, τ ,are merely the types described above. Universally quantified schemes, ∀ α.σ , bind thevariable α within the scheme σ . Naturally, it is through universal quantification that BCLsupports parametric polymorphism. Terms
The grammar of the terms in BCL is given in Fig. 5. Most of the term forms aredrawn directly from the lambda calculus. Term variables are ranged over by x , y , z , etc.,and are introduced by lambda abstraction, let-bindings and match-expressions. Lambda utting gradual types to work 9 t ::= x | λx.t | t t | let rec x = t in t | t : τ | { l i : t i } | t.l | l t | match t with l i x i ⇒ t i Fig. 5: Grammar of termsabstraction is written λx.t and binds x within the expression t . Lambda abstractionsare eliminated by application, which is denoted by juxtaposition: t t . Let-bindings arewritten let rec x = t in t . The rec is optional and, when present, indicates x may bereferenced by the expression to the right of the = ; x may of course always be referencedby the expression to the right of the in . Type annotations are written t : τ , and serve toensure that t has the they τ .In addition to the forms described above, BCL supports records and variants. Recordintroduction is written { l i : t i } , where t i evaluates to the value stored in the field l i . Records are eliminated by field projection. The projection of the field l from therecord t is written t.l . Variant introduction is written l t , where the label l is used totag the variant’s payload, t . Variants are eliminated by case analysis, which is written match t with l i x i ⇒ t i , which evaluates to the branch specified by the tag associatedwith the variant t . We identify three main components required to add gradual typing to a statically typedlanguage with type inference, such as BCL. The first is the ability to annotate termswith types, as these annotations dictate whether a term is type-checked statically ordynamically. The second is the addition of a dynamic type to the existing set of types,and the third is an algorithm to unify the existing types with the newly added dynamictype. Since our grammar, shown in Fig. 5, already supports explicit annotation of terms,we have the means to differentiate between dynamically typed and statically typed code.We add a dynamic type, ? , to our set of types; it serves to indicate that a term that willbe dynamically typed. BCL’s type inference algorithm statically infers a type for everyterm, meaning that by default BCL programs are completely statically typed. In order totell the type system to dynamically type some terms, we must explicitly annotate thoseterms with the ? type.For example: a simple increment function can be defined in BCL as follows. let incr x = x + 1 in incr The type system will infer the type
Int → Int for the incr function. However, wecan instead provide an explicit annotation. let incr x = x + 1 in incr : ? -> Int In this case, the inference algorithm retains the annotated type as the type of thefunction. Any type checks on the argument of the incr function would be put offuntil runtime. While the type checks pertaining to ? types are delayed, we still need tocomplete the inference procedure in order to infer the types of the un-annotated portionsof the program (like the return type of incr ). Siek and Vacchrajani [25](S&V) extendthe standard unification-based inference algorithm to handle the ? type. Their algorithm is based on the consistent-equal relation which takes into consideration the type variablesthat are generated as part of a typical type inference algorithm. Fortunately for us, theiralgorithm works well for our implementation with only minor adaptations. maybe_copy_dyns ( τ (cid:39) τ ) = τ (cid:48) ← i f was_copied τ then τ e l s e copy_dyn τ τ (cid:48) ← i f was_copied τ then τ e l s e copy_dyn τ τ (cid:48) (cid:39) τ (cid:48) unify τ (cid:48)(cid:48) τ (cid:48)(cid:48) = τ ← find τ (cid:48)(cid:48) τ ← find τ (cid:48)(cid:48) i f was_visited τ and was_visited τ then ( ) e l s e c a s e maybe_copy_dyns ( τ (cid:39) τ ) o f α (cid:39) τ | τ (cid:39) α ⇒ merge τ α ( * Case 1 & 2 * ) | ? (cid:39) τ → τ | τ → τ (cid:39) ? ⇒ ( * Case 3 & 4 * ) unify τ ( new ? ) unify τ ( new ? ) | ? (cid:39) τ | τ (cid:39) ? ⇒ merge τ ? ( * Case 5 & 6 * ) | τ → τ (cid:39) τ → τ ⇒ ( * Case 7 * ) unify τ τ unify τ τ | l : τ ; τ (cid:39) l (cid:48) : τ (cid:48) ; τ (cid:48) i f l = l (cid:48) ⇒ ( * Case 8 * ) unify τ τ (cid:48) unify τ τ (cid:48) | l : τ ; τ (cid:39) l (cid:48) : τ (cid:48) ; τ (cid:48) ⇒ ( * Case 9 * ) α ← fresh_type_variable ( ) unify ( l : τ ; α ) τ (cid:48) unify ( l (cid:48) : τ (cid:48) ; α ) τ | µα.τ (cid:39) τ (cid:48) | τ (cid:48) (cid:39) µα.τ ⇒ ( * Case 10 & 1 1 * ) mark_visited ( µα.τ ) unify τ [ µα.τ /α ] τ (cid:48) | (cid:15) (cid:39) (cid:15) ⇒ ( ) ( * Case 1 2 * ) | _ ⇒ e r r o r infer Γ t = c a s e t o f . . . t : τ → c a s e ( unify ( infer Γ t ) τ ) o f E r r o r ⇒ E r r o r : i n c o n s i s t e n t t y p e s | _ ⇒ τ Fig. 6: Type inference algorithm utting gradual types to work 11
Fig. 6 shows an outline of our adaptation of S&V’s inference algorithm. Unlike theoriginal algorithm by S&V, BCL’s does not separate constraint generation and constraintsolving. This difference is important, as it means that our inference algorithm doesnot have access to the whole constraint set prior to unification. Instead the infer function traverses the term, generating and solving constraints on the fly. For example, ifit encounters an application t t , it figures out the type of the term from the environment( Γ ) and generates a constraint like { τ (cid:39) τ → α } , where τ is the type of term t , τ is the type of t and α is a fresh type variable. infer sends this constraint to unify ,which attempts to satisfy it or raises an error if the constraint cannot be satisfied.Fig. 6 shows the infer case for a term t annotated with the type τ . infer generatesa constraint which tries to unify the type inferred for t with the annotated type, τ . Wehighlight this case for two reasons. First, the only way we can currently introduce a ? type in BCL is through an annotation. Therefore, this is the only point where constraintsinvolving the ? type originate. Second it is critically important that this case returns theannotated type and not the inferred type. Note that in incr the inferred type Int → Int differs from–but is consistent-equal with–the annotated type ? → Int . We always wantthe user’s explicit annotation to take precedence in this situation.BCL’s unification algorithm is already based on Huet’s unification algorithm, whichmakes adopting the changes suggested by S&V easier. The crux of S&V’s algorithmlies in the way the ? type unifies with other types, and particularly with type variables.When ? unifies with a type variable, S&V’s algorithm makes ? the representative node.However, when ? unifies with types other than type variables, the other type becomesthe representative element of the resulting set. The find and merge functions in Fig. 6come from the union-find data structure that underlies Huet’s unification algorithm.Respectively, they compute a node’s representative element, and union two nodes’ setskeeping the representative element of the first node’s set.The first six cases of the unify function handle unification with the ? type as laidout by S&V. We say first six because Cases 1 and 2 take care of unifying the ? type withtype variables as specified by S&V’s algorithm. Cases 3 and 4 handle an edge case in theiralgorithm. These two cases simulate the operational semantics of Siek and Taha [23],which require constraints like { ? (cid:39) α → β } to be treated as { ? → ? (cid:39) α → β } . Weuse new to create a new node different from what was passed in to handle this case.Cases 8-11 take care of unifying with row and recursive types, neither of which arecovered by S&V’s solution. However, it is our observation that these types do not requirespecial handling. A constraint like { x : Int ; (cid:15) (cid:39) ? } would be handled by Case 2 and ? would be merged with the row type x : Int ; (cid:15) . Now suppose the ? is present inside therow type like in the following constraint { x : ? ; (cid:15) (cid:39) x : Int ; (cid:15) } ; this will be handled byCase 8 and then Cases 5 and 12 when we recursively call unify with the types withinthe row. The same holds true for unification with the recursive type. For example, a Put another way, our inference algorithm solves each constraint immediately after generating it,and before generating the next constraint.2 Shivkumar et al. constraint like { List Int (cid:39)
List ? } will have the following successful unification trace: { List Int (cid:39)
List ? } (Case 10) → { Π ( head : Int ; tail : List Int ; (cid:15) ) (cid:39) List ? } (Case 11) → { Π ( head : Int ; tail : List Int ; (cid:15) ) (cid:39) Π ( head : ? ; tail : List ? ; (cid:15) ) } (Case 8) → {{ Int (cid:39) ? } , { List Int (cid:39)
List ? }} (Case 6, Case 10) → · · ·→ { Π (cid:15) (cid:39)
Π (cid:15) } (Case 8) → { (cid:15) (cid:39) (cid:15) } → () (Case 12) Where
List is defined as follows.
List α ≡ µa.Σ ( N il : Π(cid:15) ; Cons : Π ( head : α ; tail : a ; (cid:15) ); (cid:15) ) Note that BCLsupports equi-recursive types , as mentioned in Section 3, so unify tracks the types itvisits with mark_visited and was_visited to detect cycles.
The copy_dyn conundrum:
The copy_dyn function is a crucial part of the way ? unifies with other types. In S&V’s presentation, copy_dyn ensures that each ? node inthe constraint set is physically unique. Without this step, multiple types might unify withthe same ? node, and then transitively with each other. This has the potential to causespurious failures in unify . S&V’s solution to this is to traverse the constraint set andduplicate each ? node prior to unification; this is performed by their implementation of copy_dyn . Unfortunately, we do not have access to the full constraint set, because ourinference algorithm generates and solves constraints in one step.Our first attempt at working around this issue was to call copy_dyn as the first stepin unification. However, this leads to over copying. For example, consider the constraint{ ? → α (cid:39) α → τ }. According to Case 7 of unify , when α unifies with the ? node, copy_dyn is called and a new ? node is created in the union-find structure. But when α then unifies with τ , find looks up ? as α ’s representative element, and copy_dyn is called once more. τ therefore unifies with the new ? node, instead of the one whichunified with α . Thus, we lose the fact that τ and α are the same type.To rectify this, we implement maybe_copy_dyns , which traverses a constraintand copies each ? node exactly once. The result of this is the same as originallyintended by S&V’s copy_dyn function. That is, we ensure there is a unique ? node inthe union-find structure for every unique use of ? . In Section 2 we gave an overview of how statically typed languages approach thisproblem of promoting dynamic typing. It is our observation that most statically typed,object-oriented languages approach dynamic typing following Abadi et al. That is, theirdynamic type exploits subtype polymorphism to bypass static type checking. This isa natural direction for object-oriented languages which rely heavily on subtyping. In There are many ways to accomplish this. Our approach was to use one canonical ? node in typeannotations, and compare each ? ’s address to the canonical node’s address before copying.utting gradual types to work 13 order to inspect the types at runtime, these languages make use of type reflection. Javais one such language where work has been done to add dynamic types using reflection,contracts and mirrors [6]. The Java Virtual Machine supports many dynamic languageslike Jython and JRuby, demonstrating that such runtime constructs help static languagesadd more dynamic type checks. However, these implementations only add dynamicchecks, and do not achieve the gradual goal of a seamless mix of static and dynamictypes as in [26]. To our knowledge, only Featherweight Java [11] has attempted tosupport proper gradual typing [12]. In any case, the primary purpose for dynamic typesin these languages is inter-operation with other dynamic languages. This differs fromour own purpose and the end result does not fit our needs well. Thus we conclude thatthis approach was not a good design choice for us.The languages closest to BCL are statically typed functional languages with typeinference, such as SML, OCaml, and Haskell. OCaml has incorporated dynamic typingat the library level by leveraging its support for generalized algebraic data types [3].Similarly, Haskell supports a dynamic type as a derivative of the Typeable type class,which uses reflection [13] to look at the runtime representation of types. While theseapproaches introduce more dynamism, they lack the simplicity of gradual typing, whichhide all the nuts and bolts of the type system under a simple ? annotation.Seamless interoperation of static and dynamic types as promised by gradual typingfits very well with our use case. It lets our end users access both paradigms withoutknowledge of specialized types or constructs. Furthermore, the approach we use—extending unification-based inference with gradual typing—is a natural extension forlanguages like BCL, which support static type inference. The addition of dynamic typesto the type system easily boils down to how we handle this new type in the unificationalgorithm, and does not require reworking the entire type system. We attribute this benefitto S&V’s proposed inference algorithm, which incorporates the essence of the λ ? → typesystem. This makes it easier to adapt to an existing language with similar constructs.Garcia and Cimini’s work takes a different approach to this problem but their endgoal is the same: gradual type inference in a statically typed language. The authorsof that work feel that S&V’s approach has “complexities that make it unclear howto adopt, adapt, and extend this approach with modern features of implicitly typedlanguages like let-polymorphism, row-polymorphism and first class polymorphism”.Our experience with S&V’s approach was different: we found the integration fairlysimple without major changes to the original inference algorithm. We leave a deep diveinto the differences between these two schemes to future work. Based on Garcia andCimini’s design principle, Xie et al. [34] introduce an inference algorithm with supportfor higher-rank polymorphism, using a consistent subtyping relation. In contrast, BCLonly infers rank-1 polymorphic types and doesn’t support higher-rank polymorphism.We recognize an added benefit of going from a static to a dynamic language withexplicit ? annotations. Promoting static type checking in a dynamic language withouttype inference requires the programmer to add annotations to all parts of the code thatthey want statically checked. Needing to add these annotations is such a burden for theprogrammer that they often skip some annotations and miss out on static optimizations.These un-annotated types are implicitly dynamic, leading to runtime overhead, despite the fact that on many occasions they could be statically inferred. This in turn has lead toefforts to making gradual typing more efficient [22].BCL does not have this issue as it provides static inference by default. It thereforeenjoys the optimizations of static typing and and can skip unnecessary runtime checks.Moreover, BCL could support a dynamic-by-default mode with an additional compilerflag that implicitly annotates un-annotated terms with the ? type. This makes it even moreseamless to go from complete static typing to complete dynamic typing. We might alsoconsider doing this implicit annotation on a file-level or function-level. In cases where itis possible to separate dynamic and static components, this could even lead to cleanerrefactoring. These ideas have not yet been implemented in BCL but are something weintend to do as future work. Gradual typing enables the quick prototyping common in dynamic languages, as wellas specialized applications that enable simplification of existing code. In this section,we focus on the latter, due to space constraints. Notice, in Fig. 3, that the scale combinator [19] is simply multiplication of a contract by a floating-point observable .In a domain specific language like BCL, it is convenient to reuse the ** multiplicationsyntax for scale as well. We can fit this more general observable multiplicationoperator into the type system with the gradual type Obs Double → ? → ? . Our newmultiplication operator can delegate to scale when the second argument a Contract at runtime and continue with observable multiplication, or raise a runtime type errorbased on the runtime type of the second argument. With this new operator, the receive function can be rewritten thus: let receive currency amount = one currency ** amount in ... There are a variety of extensions to Hindley-Milner that enable this sort of ad-hocpolymorphism statically. Type classes, for example, extend the signature of overloadedfunctions with classes [31], which our users would need to learn. Similarly, modularimplicits introduce a separate syntax for implicit modular arguments [33]. However,these constructs require effort to educate our end users in their use and detract from thedynamic feel of the language. Gradual types, by contrast, are much easier for our endusers since they already work in a dynamic environment and it does not require newsyntax (save a ? annotation).It is worth noting that, while the new multiplication operator can be given a validtype in BCL, it cannot currently be implemented in BCL; it can only be implemented asa built-in operator because BCL provides no way to perform case analysis on the runtimetype of a dynamic value. However, addressing this is actually quite easy if we reuse BCL’sexisting support for variants. That is, we could implement a dynamic_to_type prim-itive which consumes a dynamic value and produces a variant describing its runtimetype. This would allow us to then branch on this variant with the existing match con-struct. Fig. 7 shows a prototype of a function that achieves this effect assuming the dynamic_to_type primitive is defined. dynamic_to_type is interesting in light of our discussion in Section 2, whichdescribes dynamic programming as the territory of BCL’s users and not its maintainers. utting gradual types to work 15 let dyn_obs_mul x y = match dynamic_to_type (y) with | Obs Double => x ** y| Contract => scale x y in dyn_obs_mul : Obs Double → Dyn → Dyn
Fig. 7: Sample of a dynamic Observable multiplication functionClearly, however, the dynamic multiplication operator is something that would live inBCL’s standard library and be maintained by the language maintainers. Indeed thereare a number of interesting standard library functions which we might add on top of dynamic_to_type . Another simple example would be a any_to_string func-tion, which could produce a string representation for arbitrary types by traversing theirruntime type and delegating to the appropriate type-specific to-string function. Such afunction would be very handy for debugging and quick inspection of values.The any_to_string example is a function which consumes an arbitrary value.However, there are equally compelling use cases for producing arbitrary values. Forexample, property-based testing frameworks rely on automatically generating valuesthat conform to certain constraints. We could implement a simple property-based testingframework with a function which consumes the output of dynamic_to_type andgenerates arbitrary values that conform to that type. Such a framework would be espe-cially useful in a domain such as ours, where real money is at stake, and where robusttesting is absolutely critical.
Dynamic languages are extremely popular with many users. For users with a limitedcomputer science background, for whom ease-of-use is the paramount, this is doublytrue. However, despite the flexibility offered by dynamic typing, the safety offered bystatic typing is helpful in domains where correctness is critical. In such an arena, gradualtypes are a perfect blend of both paradigms, and they provides a middle ground toplease a larger group of users. Given this, it is important for the literature to speak aboutadapting gradual types to existing languages. As a first step towards that, we write aboutour experiences adapting gradual typing to our implementation of a statically typedfunctional language with type inference. We provide context in terms of how others insimilar situations approached this problem, and we elaborate our inference algorithmwith key insights around what worked for us and what did not. We identify an interestinguse case for gradual types here at Bloomberg, where we look to harmonize end usersand language maintainers with competing goals. End users want to specify financialcontracts without worrying about static typing demands, while language maintainersneed a more rigorous type system that ensures that libraries that they write are error-free.Gradual types allow us to satisfy both groups. We also intend to gather feedback fromour end users and maintainers about how gradual types are being used , which can giveinsight into possible tweaks to make this system more amenable to all.
References
1. Using type dynamic. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/using-type-dynamic , accessed: 2020-10-022. Abadi, M., Cardelli, L., Pierce, B., Plotkin, G.: Dynamic typing in a statically typed language.ACM transactions on programming languages and systems (TOPLAS) (2), 237–268 (1991)3. Balestrieri, F., Mauny, M.: Generic programming in ocaml. Electronic Proceedings in The-oretical Computer Science , 59–100 (Dec 2018). https://doi.org/10.4204/eptcs.285.3, http://dx.doi.org/10.4204/EPTCS.285.3
4. Damas, L., Milner, R.: Principal type-schemes for functional programs. In: Proceedings ofthe 9th ACM SIGPLAN-SIGACT symposium on Principles of programming languages. pp.207–212 (1982)5. Garcia, R., Cimini, M.: Principal type schemes for gradual programs. SIGPLAN Not. (1),303–315 (Jan 2015). https://doi.org/10.1145/2775051.2676992, https://doi.org/10.1145/2775051.2676992
6. Gray, K.E., Findler, R.B., Flatt, M.: Fine-grained interoperability through mirrors and con-tracts. ACM SIGPLAN Notices (10), 231–245 (2005)7. Greenberg, M.: The dynamic practice and static theory of gradual typing. In: 3rd Summit onAdvances in Programming Languages (SNAPL 2019). Schloss Dagstuhl-Leibniz-Zentrumfuer Informatik (2019)8. Gronski, J., Knowles, K., Tomb, A., Freund, S.N., Flanagan, C.: Sage: Hybrid checking forflexible specifications. In: Scheme and Functional Programming Workshop. vol. 6, pp. 93–104(2006)9. Hejlsberg, A.: C (2010)10. Huet, G.: A unification algorithm for typed lambda calculus. Theoretical Computer Science (1), 27 – 57 (1975). https://doi.org/https://doi.org/10.1016/0304-3975(75)90011-0,
11. Igarashi, A., Pierce, B.C., Wadler, P.: Featherweight java: A minimal core cal-culus for java and gj. ACM Trans. Program. Lang. Syst. (3), 396–450 (May2001). https://doi.org/10.1145/503502.503505, https://doi.org/10.1145/503502.503505
12. Ina, L., Igarashi, A.: Gradual typing for featherweight java. Computer Software (2), 18–40(2009)13. Jones, S.P., Weirich, S., Eisenberg, R.A., Vytiniotis, D.: A reflection on types. In: A List ofSuccesses That Can Change the World, pp. 292–317. Springer (2016)14. Knight, K.: Unification: A multidisciplinary survey. ACM Computing Surveys (CSUR) (1),93–124 (1989)15. Matthews, J., Findler, R.B.: Operational semantics for multi-language programs. ACM Trans-actions on Programming Languages and Systems (TOPLAS) (3), 1–44 (2009)16. Meijer, E., Drayton, P.: Static typing where possible, dynamic typing when needed: The endof the cold war between programming languages. Citeseer (2004)17. Milner, R.: A theory of type polymorphism in programming. Journal of computer and systemsciences (3), 348–375 (1978)18. Miyazaki, Y., Sekiyama, T., Igarashi, A.: Dynamic type inference for gradual hindley–milnertyping. Proc. ACM Program. Lang. (POPL) (Jan 2019). https://doi.org/10.1145/3290331, https://doi.org/10.1145/3290331
19. Peyton Jones, S., Eber, J.M., Seward, J.: Composing contracts: An adventurein financial engineering (functional pearl). SIGPLAN Not. (9), 280–292 (Sep2000). https://doi.org/10.1145/357766.351267, https://doi.org/10.1145/357766.351267 utting gradual types to work 1720. Pottier, F.: A modern eye on ml type inference: old techniques and recent developments (092005)21. Pottier, F., Rémy, D.: The Essence of ML Type Inference, pp. 389–489 (01 2005)22. Rastogi, A., Chaudhuri, A., Hosmer, B.: The ins and outs of gradual type inference. SIGPLANNot. (1), 481–494 (Jan 2012). https://doi.org/10.1145/2103621.2103714, https://doi.org/10.1145/2103621.2103714
23. Siek, J., Taha, W.: Gradual typing for functional languages. i: Scheme and functional pro-gramming workshop (2006)24. Siek, J., Taha, W.: Gradual typing for objects. In: European Conference on Object-OrientedProgramming. pp. 2–27. Springer (2007)25. Siek, J.G., Vachharajani, M.: Gradual typing with unification-based inference. In: Proceedingsof the 2008 symposium on Dynamic languages. pp. 1–12 (2008)26. Siek, J.G., Vitousek, M.M., Cimini, M., Boyland, J.T.: Refined Criteria for Gradual Typing.In: Ball, T., Bodik, R., Krishnamurthi, S., Lerner, B.S., Morrisett, G. (eds.) 1st Summiton Advances in Programming Languages (SNAPL 2015). Leibniz International Proceed-ings in Informatics (LIPIcs), vol. 32, pp. 274–293. Schloss Dagstuhl–Leibniz-Zentrum fuerInformatik, Dagstuhl, Germany (2015). https://doi.org/10.4230/LIPIcs.SNAPL.2015.274, http://drops.dagstuhl.de/opus/volltexte/2015/5031
27. Tarjan, R.E.: Efficiency of a good but not linear set union algorithm. Journal of the ACM(JACM) (2), 215–225 (1975)28. Tobin-Hochstadt, S., Felleisen, M.: Interlanguage migration: from scripts to programs. In:Companion to the 21st ACM SIGPLAN symposium on Object-oriented programming systems,languages, and applications. pp. 964–974 (2006)29. Tobin-Hochstadt, S., Felleisen, M.: The design and implementation of typed scheme. ACMSIGPLAN Notices (1), 395–406 (2008)30. Vitousek, M.M., Kent, A.M., Siek, J.G., Baker, J.: Design and evaluation of gradualtyping for python. In: Proceedings of the 10th ACM Symposium on Dynamic Lan-guages. p. 45–56. DLS ’14, Association for Computing Machinery, New York, NY,USA (2014). https://doi.org/10.1145/2661088.2661101, https://doi.org/10.1145/2661088.2661101
31. Wadler, P., Blott, S.: How to make ad-hoc polymorphism less ad hoc. In: Proceedings ofthe 16th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages.p. 60–76. POPL ’89, Association for Computing Machinery, New York, NY, USA (1989).https://doi.org/10.1145/75277.75283, https://doi.org/10.1145/75277.75283
32. Wand, M.: A simple algorithm and proof for type inference (1987)33. White, L., Bour, F., Yallop, J.: Modular implicits. Electronic Proceedings in TheoreticalComputer Science , 22–63 (Dec 2015). https://doi.org/10.4204/eptcs.198.2, http://dx.doi.org/10.4204/EPTCS.198.2
34. Xie, N., Bi, X., Oliveira, B.C.D.S., Schrijvers, T.: Consistent subtyping for all. ACMTrans. Program. Lang. Syst. (1) (Nov 2019). https://doi.org/10.1145/3310339,(1) (Nov 2019). https://doi.org/10.1145/3310339,