Local type inference(Pierce and Turner, 2000) is a simple yet effective partial technique for inferring types for programs. In contrast to complete methods of type inference such as the Damas-Milner system(Damas and Milner, 1982) which can type programs without any type annotations by restricting the language of types, partial methods require the programmer to provide some type annotations and, in exchange, are suitable for use in programming languages with rich type features such as impredicativity and subtyping(Pierce and Turner, 2000; Odersky et al., 2001), dependent types(Xi and Pfenning, 1999), and higher-rank types(Peyton Jones et al., 2005), where complete type inference may be undecidable.
Local type inference is also contrasted with global inference methods (usually based on unification) which are able to infer more missing annotations by solving typing constraints generated from the entire program. Though more powerful, global inference methods can also be more difficult for programmers to use when type inference fails, as they can generate type errors whose root cause is distant from the location the error is reported(McAdam, 2002). Local type inference address this issue by only propagating typing information between adjacent nodes of the abstract syntax tree (AST), allowing programmers to reason locally about type errors. It achieves this by using two main techniques: bidirectional type inference rules and local type-argument inference.
The first of these techniques, bidirectional type inference, is not unique to local type inference ((Dunfield and Krishnaswami, 2013; Peyton Jones et al., 2005; Vytiniotis et al., 2006; Xie and Oliveira, 2018) are just a few examples), and uses two main judgment forms, often called synthesis and checking mode. When a term synthesizes type , we view this typing information as coming up and out of and as available for use in typing nearby terms; when checks against type (called in this paper the contextual type), this information is being pushed down and in to and is provided by nearby terms.
The second of these techniques, local type-argument inference, finds the missing types arguments in polymorphic function applications by using only the type information available at an application node of the AST. For a simple example, consider the expression where id has type and has type ℕ. Here we can perform synthetic type-argument inference by synthesizing the type of and comparing this to the type of pair to infer that the missing type argument instantiating is ℕ.
Using these two techniques, local type inference has a number of desirable properties. Though some annotations are still required, in practice a good number of type annotations can be omitted, and often those that need to remain are predictable and coincide with programmers’ expectations that they serve as useful and machine-checked documentation(Pierce and Turner, 2000; Hosoya and Pierce, 1999). Without further instrumentation, local type inference already tends to report type errors close to where further annotations are required; more recently, it has been used in (Plociniczak, 2016) as the basis for developing autonomous type-driven debugging and error explanations. The type inference algorithms of (Pierce and Turner, 2000; Odersky et al., 2001) admit a specification for their behavior, helping programmers understand why certain types were inferred without requiring they know every detail of the type-checker’s implementation. Add to this its relative simplicity and robustness when extended to richer type systems and it seems unsurprising that it has been a popular choice for type inference in programming languages.
Unfortunately, local type inference can fail even when it seems like there should be enough typing information available locally. Consider trying to check that the expression has type , assuming pair has type ). The inference systems presented in (Odersky et al., 2001; Pierce and Turner, 2000) will fail here because the argument
does not synthesize a type. The techniques proposed in the literature of local type inference for dealing with cases similar to this include classifying and avoiding such “hard-to-synthesize” terms(Hosoya and Pierce, 1999) and utilizing the partial type information provided by polymorphic functions(Odersky et al., 2001); the former was dismissed as unsatisfactory by the same authors that introduced it and the latter is of no help in this situation, since the type of pair tells us nothing about the expected type of . What we need in this case is contextual type-argument inference, utilizing the information available from the expected type of the whole application to know argument is expected to have type .
Additionally, languages using local type inference usually use fully-uncurried applications in order to maximize the notion of “locality” for type-argument inference, improving its effectiveness. The programmer can still use curried applications if desired, but “they are second-class in this respect.”(Pierce and Turner, 2000). It is also usual for type arguments to be given in an “all or nothing” fashion in such languages, meaning that even if only one cannot be inferred, all must be provided. We believe that currying and partial type applications are useful idioms for functional programming and wish to preserve them as first-class language features.
In this paper, we explore the design space of local type inference in the setting of System F(Girard, 1986; Girard et al., 1989) by developing spine-local type inference, an approach that both expands the locality of type-argument inference to an application spine and augments its effectiveness by using the contextual type of the spine. In doing so, we
show that we can restore first-class currying, partial type applications, and infer the types for some “hard-to-synthesize” terms not possible in other variants of local type inference;
provide a specification for contextual type-argument inference with respect to which we show our algorithm is sound and complete
give a weak completeness theorem for our type system with respect to fully annotated System F programs, indicating the conditions under which the programmer can expect type inference succeeds and where additional annotations are required when it fails.
Spine-local type inference is being implemented in Cedille(Stump, 2017), a functional programming language with higher-order and impredicative polymorphism and dependent types and intersections. Though the setting for this paper is much simpler, we are optimistic that spine-local type inference will serve as a good foundation for type inference in Cedille that makes using its rich type features more convenient for programmers.
The rest of this paper is organized as follows: in Section 2 we cover the syntax and some useful terminology for our setting; in Section 3 we present the type inference rules constituting a specification for contextual type-argument inference, consider its annotation requirements, and illustrate its use, limitations, and the type errors it presents to users; in Section 4 we show the prototype-matching algorithm implementing contextual type-argument inference; and in Section 5 we discuss how this work compares to other approaches to type inference.
2. Internal and External Language
Type inference can be viewed as a relation between an internal language of terms, where all needed typing information is present, and an external language, in which programmers work directly and where some of this information can be omitted for their convenience. Under this view, type inference for the external language not only associates a term with some type but also with some elaborated term in the internal language in which all missing type information has been restored. In this section, we present the syntax for our internal and external languages as well as introduce some terminology that will be used throughout the rest of this paper.
We take as our internal language explicitly typed System F (see (Girard et al., 1989)); we review its syntax below:
Types consist of type variables, arrow types, and type quantification, and typing contexts consist of the empty context, type variables (also called the context’s declared type variables), and term variables associated with their types. The internal language of terms consists of variables, λ-abstractions with annotations on bound variables, Λ-abstractions for polymorphic terms, and term and type applications. Our notational convention in this paper is that term meta-variable indicates an elaborated term for which all type arguments are known, and indicates a partially elaborated term where some type arguments are type meta-variables (discussed in Section 3).
The external language contains the same terms as the internal language as well as bare λ-abstractions – that is, λ-abstractions missing an annotation on their bound variable:
Types and contexts are the same as for the internal language and are omitted.
In both the internal and external languages, we say that the applicand of a term or type application is the term in the function position. A head a is either a variable or λ-abstraction (bare or annotated), and an application spine(Cervesato and Pfenning, 2003) (or just spine) is a view of an application as consisting of some head (called the spine head) followed by a sequence of (term and type) arguments. The maximal application of a sub-expression is the spine in which it occurs as an applicand, or just the sub-expression itself if it does not. For example, spine is the maximal application of itself and its applicand sub-expressions , , and , with as head of the spine. Predicate indicates term is some term or type application (in either language) and we define it formally as .
Turning to definitions for types and contexts, function calculates the set of declared type variables of context and is defined recursively by the following set of equations:
Predicate indicates that type is well-formed under – that is, all free type variables of occur as declared type variables in (formally ).
3. Type Inference Specification
The typing rules for our internal language are standard for explicitly typed System F and are omitted (see Ch. 23 of (Pierce, 2002) for a thorough discussion of these rules). We write to indicate that under context internal term has type . For type inference in the external language, Figure 1 shows judgment which consists mostly of standard (except for and ) bidirectional inference rules with elaboration to the internal language, and Figure 2 shows the specification for contextual type-argument inference. Judgment in Figure 1(b) handles traversing the spine and judgment in Figure 1(c) types its term applications and performs type-argument inference (both synthetic and contextual). Figure 1(a) gives a “shim” judgment which bridges the bidirectional rules with the specification for rhetorical purposes (discussed below). Though these rules are not algorithmic, they are syntax-directed, meaning that for each judgment the shape of the term we are typing (i.e. the subject of typing) uniquely determines the rules that applies.
We now consider more closely each judgment form and its rules starting with , the point of entry for type inference. The two modes for type inference, checking and synthesizing, are indicated resp. by (suggesting pushing a type down and into a term) and (suggesting pulling a type up and out of a term). Following the notational convention of Peyton Jones et al.(Peyton Jones et al., 2005) we abbreviate two inference rules that differ only in their direction to one by writing , where is a parameter ranging over . We read judgment as: “under context , term synthesizes type and elaborates to ,” and a similar reading for checking mode applies for . When the direction does not matter, we will simply say that we can infer has type .
Rule is standard. Rule says we can infer missing type annotation on a λ-abstraction when we have a contextual arrow type . Rules and say that Λ- and annotated λ-abstractions can have their types either checked or synthesized. says that a type application has its type inferred in either mode when the applicand synthesizes a quantified type. The reason for this asymmetry between the modes of the conclusion and the premise is that even when in checking mode, it is not clear how to work backwards from type to .
and are invoked on maximal applications and are the first non-standard rules. To understand how these rules work, we must 1) explain the “shim” judgment serving as the interface for spine-local type-argument inference and 2) define meta-language function . Read as: “under context and with (optional) contextual type , partially infer application has type with elaboration and solution ,” where is a substitution mapping a some meta-variables (i.e. omitted type arguments) in to contextually-inferred type arguments.
In rule , is provided to indicating no contextual type is available. We constrain to be the identity substitution (written ) and that elaborated term has no unsolved meta-variables, matching our intuition that all type arguments must be inferred synthetically. In rule , we provide the contextual type to and check (implicitly) that it equals and (explicitly) that all remaining meta-variables in are solved by , then elaborate (the replacement of each meta-variable in with its entry in ). Shared by both is the second premise of the (anonymous) rule introducing that solves precisely the meta-variables of the partially inferred type for application .
What are the “meta-variables” of elaborations and types? When is a term application with some type arguments omitted in its spine, its partial elaboration from spine-local type-argument inference under context fills in each missing type argument with either a well-formed type or with a meta-variable (a type variable not declared in ) depending on whether it was inferred synthetically. For example, if and we wanted to check that it has type under a typing context associating pair with type and with type ℕ, then we could derive
(assuming some base type ℕ, some family of base types for all types and , and assuming is not declared in .) Looking at the partial elaboration of , we would see that type argument was inferred from its contextual type and that was inferred from the synthesized type of the arguments to pair.
Meta-variables never occur in a judgment formed by , only in the judgments of Figure 2. In particular, these rules enforce that meta-variables in a partial elaboration can occur only as type arguments in its spine, not within its head or term arguments. This restriction guarantees spine-local type-argument inference and helps to narrow the programmer’s focus when debugging type errors. Furthermore, meta-variables correspond to omitted type arguments injectively, significantly simplifying the kind of reasoning needed for debugging type errors. We make this precise by defining meta-language function which yields the set of meta-variables occurring in its second argument with respect to the context . is overloaded to take both types and elaborated terms for its second argument: for types we define , the set of free variables in less the declared type variables of ; for terms, is defined recursively by the following equations:
Using our running example where the subject is we can now show how the meta-variable checks are used in rules and . We have for our partially elaborated term that and also for our type that . If we have a derivation of the judgment above formed by we can then derive with rule
because substitution solves the remaining meta-variable in the elaborated term and type, and when utilized on the partially inferred type yields the contextual type for the term. However, we would not be able to derive with rule
since we do not have as our solution and we have meta-variable remaining in our partial elaboration and type. Together, the checks in and ensure that meta-variables are never passed up and out of a maximal application during type inference.
Judgment serves as an interface to spine-local type-argument inference. In Figure 1(a) it is defined in terms of the specification for contextual type-argument inference given by judgments and ; we call it a “shim” judgment because in Figure 3(a) we give for it an alternative definition using the algorithmic rules in which the condition is not needed. Its purpose, then, is to cleanly delineate what we consider specification and implementation for our inference system.
Though the details of maintaining spine-locality and performing synthetic type-argument inference permeate the inference rules for and , these rules form a specification in that they fully abstract away the details of contextual type-argument inference, describing how solutions are used but omitting how they are generated. Spine-locality in particular contributes to our specification’s perceived complexity – what would be one or two rules in a fully-uncurried language with all-or-nothing type argument applications is broken down in our system in to multiple inference rules to support currying and partial type applications.
Judgment contains three rules and serves to dig through a spine until it reaches its head, then work back up the spine typing its term and type applications. The reading for it is the same as for , less the optional contextual type. Rule types the spine head by deferring to ; our partial solution is since no meta-variables are present in a judgment formed by . is similar to except it additionally propagates solution . Rule is used for term applications: first it partially synthesizes a type for the applicand and then it uses judgment to ensure that the elaborated term with this type can be applied to argument .
Judgment performs synthetic and contextual type-argument inference and ensures that term applications with omitted type arguments are well-typed. We read as “under context , elaborated applicand of partial type together with solution can be applied to term ; the application has type and elaborates with solution .”
Contextual type-argument inference happens in rule , which says that when the applicand has type we can choose to guess any well-formed for our contextual type argument by picking (indicating contains all the mappings present in and an additional mapping for ), or choose to attempt to synthesize it later from an argument by picking . The details of which to guess, or whether we should guess at all, are not present in this specificational rule. In both cases, we elaborate the applicand to of type and check that it can be applied to – we do this even when we guess for to maintain the invariant that for all elaborations and solutions generated from the rules in Figures 1(b) and 1(c) we have , which we need when checking in the (specificational) rule for that these guessed solutions are ultimately justified by the contextual type (if any) of our maximal application.
We illustrate the use of with an example: if the input presented to judgment is
then after two uses of rule where we guess for and decline to guess for we would generate:
After working through omitted type arguments, requires that we eventually reveal some arrow type to type a term application. When it does we have two cases, handled resp. by and : either the domain type of applicand together with solution provide enough information to fully know the expected type for argument (i.e. ), or else they do not and we have some non-empty set of unsolved meta-variables in corresponding to type arguments we must synthesize. Having full knowledge, in we check has type ; otherwise, in we try to solve meta-variables by synthesizing a type for and checking it is instantiation
(vectorized notation for the simultaneous substitution of typesfor ) of . Once done, we conclude with result type and elaboration for the application, as the meta-variables of corresponding to omitted type arguments have now been fully solved by type-argument synthesis. Together, and prevent meta-variables from being passed down to term argument , as we require that it either check against or synthesize a well-formed type.
We illustrate the use of rule with and example: suppose that under context the input presented to judgment is
and furthermore that . Then we have instantiation from synthetic type-argument inference and use it to produce for the application the result type and the elaboration . Note that synthesized type arguments are used eagerly, meaning that the typing information synthesized from earlier arguments can in some cases be used to infer the types of later arguments in checking mode (see Section 3.2). This is reminiscent of greedy type-argument inference for type systems with subtyping(Cardelli, 1997; Dunfield, 2009), which is known to cause unintuitive type inference failures due to sub-optimal type arguments (i.e. less general wrt to the subtyping relation) being inferred. As System F lacks subtyping, this problem does not affect our type inference system and we can happily utilize synthesized type arguments eagerly (see Section 5).
3.1. Soundness, Weak Completeness, and Annotation Requirements
The inference rules in Figure 2 for our external language are sound with respect to the typing rules for our internal language (i.e. explicitly typed System F), meaning that elaborations of typeable external terms are typeable at the same type111A complete list of proofs for this paper can be found in the proof appendix at TODO:
Theorem 3.1 ().
(Soundness of ):
If then .
Our inference rules also enjoy a trivial form of completeness that serves as a sanity-check with respect to the internal language: since any term in the internal language (i.e., any fully annotated term) is also in the external language, we expect that should be typable using the typing rules for external terms:
Theorem 3.2 ().
(Trivial Completeness of ):
A more interesting form of completeness comes from asking which external terms can be typed – after all, this is precisely what a programmer needs to know when trying to debug a type inference failure! Since our external language contains terms without any annotations and our type language is impredicative System F, we know from (Wells, 1998) that type inference is in general undecidable. Therefore, to state a completeness theorem for type inference we must first place some restrictions on the set of external terms that can be the subject of typing.
We start by defining what it means for to be a partial erasure of internal term . The grammar given in Section 2 for the external language does not fully express where we hope our inference rules will restore missing type information. Specifically, the rules in Figures 1 and 2 will try to infer annotations on bare λ-abstractions and only try to infer missing type arguments that occur in the applicand of a term application. For example, given (well-typed) internal term and external term , our inference rules will try to infer the missing type arguments and but will not try to infer the missing .
A more artificial restriction on partial erasures is that the sequence of type arguments occurring between two terms in an application can only be erased in a right-to-left fashion. For example, given internal term , the external term is a valid erasure ( and are erased between and , and between and rightmost is erased), but term is not. This restriction helps preserve soundness of the external type inference rules by ensuring that every explicit type argument preserved in an erasure of an internal term instantiates the same type variable it did in ; it is artificial because we could instead have introduced notation for “explicitly erased” type arguments in the external language, such as , to indicate the first type argument has been erased, but did not to simplify the presentation of our inference rules and language.
The above restrictions for partial erasure are made precise by the functions and which map an internal term to sets of partial erasures . They are defined mutually recursively below:
We are now ready to state a weak completeness theorem for typing terms in the external language which over-approximates the annotations required for type inference to succeed (we write to mean some number of type quantifications over type )
Theorem 3.3 ().
(Weak completeness of ):
Let be a term of the internal language and be a term of the internal languages such that . If then when the following conditions hold for each sub-expression of , corresponding sub-expression of , and corresponding sub-derivation of :
If for some and , then for some
If occurs as a maximal term application in and if
for some and , then .
If is a term application and for some and , and if for some and , then for some and .
If is a type application and for some and , and for some and , then for some .
Theorem 3.3 only considers synthetic type-argument inference, and in practice condition (1) is too conservative thanks to contextual type-argument inference. Though a little heavyweight, our weak completeness theorem can be translated into a reasonable guide for where type annotations are required when type synthesis fails. Conditions (3) and (4) suggest that when the applicand of a term or type application already partially synthesizes some type, the programmer should give enough type arguments to at least reveal it has the appropriate shape (resp. a type arrow or quantification). (2) indicates that type variables that do not occur somewhere corresponding to a term argument of an application should be instantiated explicitly, as there is no way for synthetic type-argument inference to do so. For example, in the expression if has type there is no way to instantiate from synthesizing argument . Finally, condition (1) we suggest as the programmer’s last resort: if the above advice does not help it is because some λ-abstractions need annotations.
Note that in conditions (2), (3), and (4) we are not circularly assuming type synthesis for sub-expressions of partial erasure succeeds in order to show that it succeeds for , only that if a certain sub-expression can be typed then we can make some assumptions about the shape of its type or elaboration. Conditions (3) and (4) in particular are a direct consequence of a design choice we made for our algorithm to maintain injectivity of meta-variables to omitted type arguments. As an alternative, we could instead refine meta-variables when we know something about the shape of their instantiation. For example, if we encountered a term application whose applicand has a meta-variable type , we know it must have some arrow type and could refine to , where and are fresh meta-variables. However, doing so means type errors may now require non-trivial reasoning from users to determine why some meta-variables were introduced in the first place.
Still, we find it somewhat inelegant that our characterization of annotation requirements for type inference is not fully independent of the inference system itself. For programmers using these guidelines, this implies that there must be some way to interactively query the type-checker for different sub-expressions of a program during debugging. Fortunately, many programming languages offer just such a feature in the form of a REPL, meaning that in practice this is not too onerous a requirement to make.
Theorem 3.3 only states when an external term will synthesize its type, but what about when a term can be checked against a type? It is clear from the typing rules in Figure 1 that some terms that fail to synthesize a type may still be successfully checked against a type. Besides typing bare λ-abstractions (which can only have their type checked), checking mode can also reduce the annotation burden implied by condition (2) of Theorem 3.3: consider again the example where has type . If instead of attempting type synthesis we were to check that it has some type then we would not need to provide an explicit type argument to instantiate .
From these observations and our next result, we have that checking mode of our type inference system can infer the types of strictly more terms than can synthesizing mode – whenever a term synthesizes a type, it can be checked against the same type.
Theorem 3.4 ().
(Checking extends synthesizing):
Successful Type Inference
We conclude this section with some example programs for which the type inference system in Figures 1 and 2 will and will not be able to type. We start with the motivating example from the introduction of checking that the expression has type , which is not possible in other variants of local type inference. For convenience, we assume the existence of a base type ℕ and a family of base types for all types and . These assumptions are admissible as we could define these types using Church encodings. A full derivation for typing this program is given in Figure 3, including the following abbreviations:
To type this application we first dig through the spine, reach the head pair, and synthesize type . No meta-variables are generated by judgment and thus there can be no meta-variable solutions, so we generate solution .
Next we type the first application, , shown in sub-derivation . In the first invocation of rule we guess solution for , and in the second invocation we decline to guess an instantiation for (in this example we could have also guessed for as this information is also available from the contextual type, but choose not to in order to demonstrate the use of all three rules of ). Then using rule we check argument against . This is the point at which the local type inference systems of (Pierce and Turner, 2000; Odersky et al., 2001) will fail: as a bare λ-abstraction this argument will not synthesize a type, and the expected type as provided by the applicand pair alone does not tell us what the missing type annotation should be. However, by using the information provided by the contextual type of the entire application we know it must have type . The resulting partial type of the application is , and we propagate solution to the rest of the derivation. Note that we elaborate the argument of this application to – we never pass down meta-variables to term arguments, keeping type-argument inference local to the spine.
In sub-derivation we type (parentheses added) where our applicand has partial type . We find that we have unsolved meta-variable as the expected type for , so we use rule and synthesize the type for . Using solution , we produce for the resulting type of the application and elaborate the application to a , wherein type argument is replaced by ℕ in the original elaborated applicand .
Finally, in rule we confirm that the only meta-variables remaining in our partial type synthesis of the application is precisely those for which we knew the solutions from the contextual type. For this example, the only remaining meta-variable in both the partially synthesized type and elaboration is , which is also the only mapping in , so type inference succeeds. We use to replace all occurrences of with in the type and elaboration and conclude that term can be checked against type .
The next example illustrates how our eager use of synthetic type-argument inference can type some terms not possible in other variants of local type inference. Consider checking that the expression has type ℕ, where rapp has type and has type ℕ. From the contextual type we know that should be instantiated to ℕ, and when we reach application , we learn that should be instantiated to ℕ from the synthesized type of . Together, this gives us enough information to know that argument should have type . Such eager instantiation is neither novel nor necessarily desirable when extended to richer types or more powerful systems of inference (see Section 5), but in our setting it is a useful optimization that we happily make for inferring the types of expressions like the one above.
Type Inference Failures
To see where type inference can fail, we again use but now ask that it synthesize its type. Rule insists that we make no guesses for meta-variables (as there is no contextual type for the application that they could have come from), so we would need to synthesize a type for argument – but our rules do not permit this! In this case the user can expect an error message like the following:
expected type: ?X error: We are not in checking mode, so bound variable x must be annotated
?X indicates an unsolved meta-variable
corresponding to type variable in the type of pair. The
situation above corresponds to condition (1) of Theorem 3.3:
in general, if there is not enough information from the type of an
applicand and the contextual type of the application spine in which it
occurs to fully know the expected types of arguments that are
λ-abstractions, then such arguments require explicit type annotations.
We next look at an example corresponding to condition (2) of Theorem 3.3, namely that the type variables of a polymorphic function that do not correspond to term arguments in an application should be instantiated explicitly. Here we will assume a family of base types for every type and , a variable right of type , and a variable of type . In trying to synthesize a type for the application the user can expect an error message like:
synthesized type: (?X + ) error: This maximal application has unsolved meta-variables
indicating that type variable requires an explicit type argument be provided. Fortunately for the programmer, and unlike the local type inference systems of (Pierce and Turner, 2000; Odersky et al., 2001), our system supports partial explicit type application, meaning that can be instantiated without also explicitly (and redundantly) instantiating . On the other hand, local type inference systems for System F(Pierce and Turner, 2000; Odersky et al., 2001) can succeed to type without additional type arguments, as they can instantiate to the minimal type (with respect to their subtyping relation) Bot. Partial type application, then, is more useful for our setting of System F where picking some instantiation for this situation would be somewhat arbitrary.
A more subtle point of failure for our algorithm corresponds to conditions (3) and (4) of Theorem 3.3. Even when the head and all arguments of an application spine can synthesize their types, the programmer may still be require to provide some additional type arguments. Consider the expression , where and . Even with some contextual type for this expression, type inference still fails because the rules in Figure 1(c) require that the type of the applicand of a term application reveals some arrow, which does not. The programmer would be met with the following error message:
applicand type: ?X error: The type of an applicand in a term application must reveal an arrow
prompting the user to provide an explicit type argument for . To make expression typeable, the programmer could write , or even – our inference rules are able to solve meta-variables introduced by explicit and even synthetic type arguments, as long as there is at least enough information to reveal a quantifier or arrow in the type of a term or type applicand.
For our last type error example, we consider the situation where the programmer has written an ill-typed program. Local type inference enjoys the property that type errors can be understood locally, without any “spooky action” from a distant part of the program. In particular, with local type inference we would like to avoid error messages like the following:
synthesized type: expected type: ?X := error: type mismatch
From this error message alone the programmer has no indication of why the expected type is ! In our type inference system we expand the distance information travels by allowing it to flow from the contextual type of an application to its arguments. As an example, the error message above might be generated when checking that the expression has type , specifically when inferring the type of the first argument. Fortunately, our notion of locality is still quite small and we can easily demystify the reason type inference expected a different type:
synthesized type: expected type: ?X := contextual match: ?X ?Y := ( )
where contextual match tells the programmer to compare to the partially synthesized and contextual return types of the application to determine why was instantiated to . A similar field, synthetic match, could tell the programmer that the type of an earlier argument informs the expected type of current one.
4. Algorithmic Inference Rules
The type inference system presented in Section 3 do not constitute an algorithm. Though the rules forming judgment indicate where and how we use contextually-inferred type arguments, they do not specify what their instantiations are or even whether this information is available to use, and it is not obvious how to work backwards from the second premise in Figure 1(a) to develop an algorithm.
Figure 4 shows the algorithmic rules implementing contextual type-argument inference. The full algorithm for spine-local type inference, then, consists of the rules in Figure 1 with the shim judgment as defined in Figure 3(a). At the heart of our implementation is our prototype matching algorithm; to understand the details of how we implement contextual type-argument inference, we must first discuss this algorithm and the two new syntactic categories it introduces, prototypes and decorated types.
4.1. Prototype Matching
Figure 3(d) lists the rules for the prototype matching algorithm. We read the judgment as: “solving for meta-variables , we match type to prototype and generate solution and decorated type ,” and we maintain the invariant that . Meta-variables can only occur in , thus these are matching (not unification) rules. The grammar for prototypes and decorated types is given below:
Prototypes carry the contextual type of the maximal application of a
spine. In the base case they are either the uninformative (as in
), indicating no contextual type, or they are informative of
type (as in ). In this way, prototypes generalize the
syntactic category we introduced earlier for optional contextual
types. We use the last prototype former as we work our way
down an application spine to track the expected arity of its head. For
example, if we wished to check that the expression
id suc x has
type , then when we reached the head
id using the
rules in Figure 3(b) we would generate for it prototype
Decorated types consist of types (also called plain-decorated types), an arrow with a regular type as the domain (as prototypes only inform us of the result type of a maximal application, not of the types of arguments), quantified types whose bound variable may be decorated with the type to which we expect to instantiate it, and “stuck” decorations. On quantifiers, decoration indicates that did not inform us of an instantiation for – we sometimes abbreviate the two cases as , where and .
To explain the role of stuck decorations, consider again
id suc x.
id has type , matching
this with prototype generates decorated type
, meaning that we only know
that will be instantiated to some type that matches . Stuck decorations occur when the expected arity of a
spine head (as tracked by a given prototype) is greater than the arity
of the type of the head and are the mechanism by which we propagate a
contextual type to a head that is “over-applied” – a not-uncommon
occurrence in languages with curried applications!
Turning to the prototype matching algorithm in Figure 3(d), rule says that we match an arrow type and prototype when we can match their codomains. Rule says that when the prototype is some type we must find an instantiation such that , and rule says that any type matches with with no solutions generated (thus we call the “uninformative” prototype). In rule we match a quantified type with a prototype by adding bound variable to our meta-variables and matching the body to the same prototype; the substitution in the conclusion, , is the solution generated from this match less its mapping for , which is placed in the decoration . For example, matching with prototype generates decorated type . Finally, rule applies when there is incomplete information (in the form of ) on how to instantiate a meta-variable; we generate a stuck decoration with identity solution .
We conclude by showing that our prototype matching rules really do constitute an algorithm: when , , and are considered as inputs then behaves like a function.
Theorem 4.1 ().
(Function-ness of ):
Given , , and , if
and , then and
4.2. Decorated Type Inference
We now discuss the rules in Figures 3(b) and 3(c) which implement contextual type-argument inference (as specified by Figures 1(b) and 1(c)) by using the prototype matching algorithm. We begin by giving a reading for judgments – read as: “under context and with prototype , synthesizes decorated type and elaborates with solution ,” where again represents the contextually-inferred type arguments.
In rule we required that the solution generated by in its premise is ; in we (implicitly) required that the contextual type is equal to ; and now with the algorithmic definition for we appear to be requiring in both that the decorated type generated by is a plain-decorated type . With the algorithmic rules, these are not requirements but guarantees that the specification makes of the algorithm:
Lemma 4.2 ().
Let be the number of prototype arrows prefixing and be the number of decorated arrows prefixing . If then
Theorem 4.3 ().
(Soundness of wrt ):
Assuming prototype inference succeeds, when we specialize in Theorem 4.3 to we have immediately by rule that ; when we specialize it to some contextual type for an application, then by the premise of we have . Theorem 4.2 and 4.3 together tell us that we generate plain-decorated types in both cases, as in particular we cannot have leading (decorated) arrows or stuck decorations with prototypes or .
Next we discuss the rules forming judgment in Figure 3(b), constituting the algorithmic version of the rules in Figure 1(b). In rule , after synthesizing a type for the application head we match this type against expected prototype (we are guaranteed the prototype has this shape since only a term application can begin a derivation of ). No meta-variables occur in initially – as we perform prototype matching these will be generated by rule from quantified type variables in and their solutions will be left as decorations in the resulting decorated type . We are justified in requiring that matching to generates empty solution since we have in general that the meta-variables solved by our prototype matching judgment are a subset of the meta-variables it was asked to solve:
Lemma 4.4 ().
In , we can infer the type of a type application when synthesizes a decorated type and is either an uninformative decoration or is precisely (that is, the programmer provided explicitly the type argument the algorithm contextually inferred). We synthesize for the type application, where we extend type substitution to decorated types by the following recursive partial function:
This definition is straightforward except for the last case dealing with stuck decorations. Here, (representing instantiations given by explicit or synthetically-inferred type arguments) may provide information on how to instantiate and this must match our current (though incomplete) information from about our contextually-inferred type arguments. For example, if we have decorated type , then would require we match with and matching would generate (plain) decorated type
The definition of substitution on decorated types is partial since prototype matching may fail (consider if we used substitution in the above example instead). When a decorated type substitution appears in the conclusion of our algorithmic rules, such as in or , we are implicitly assuming an additional premise that the result is defined.
The last rule for judgment is , and like it benefits from a reading for judgment occurring in its premise. We read as: “under , elaborated applicand of decorated type together with solution can be applied to ; the application has decorated type and elaborates with solution .” Thus, says that to synthesize a decorated type for a term application we synthesize the decorated type of the applicand and ensure that the resulting elaboration , along with its decorated type and solution, can be applied to .
We now turn to the rules for the last judgment of our algorithm. Rule clarifies the non-deterministic guessing done by the corresponding specificational rule : the contextually-inferred type arguments we build during contextual type-argument inference are just the accumulation of quantified type decorations. The solution we provide to the second premise of contains mapping if is an informative decoration, and as we did in rule we provide elaborated term to track the contextually-inferred type arguments separately from those synthetically inferred.
Rule works similarly to : when the only meta-variables in the domain of our decorated type are solved by , we can check that argument has type . In rule we have some meta-variables in not solved by – we synthesize a type for the argument, ensure that it is some instantiation of , and use this instantiation on the meta-variables in as well as the decorated codomain type , potentially unlocking some stuck decoration to reveal more arrows or decorated type quantifications.
We conclude this section by noting that the specificational and algorithmic type inference system are equivalent, in the sense that they type precisely the same set of terms:
Theorem 4.5 ().
(Soundness of wrt ):
Theorem 4.6 ().
(Completeness of wrt ):
(where indicates is defined as in Figure 3(a))
Taken together, Theorems 4.5 and 4.6 justify our claim that the rules of Figure 2 constitute a specification for contextual type-argument inference – it is not necessary that the programmer know the notably more complex details of prototype matching or type decoration to understand how contextual type arguments are inferred. Indeed, the judgment provides more flexibility in reasoning about type inference than does , as in rule we may freely decline to guess a contextual type argument even when this would be justified and instead try to learn it synthetically. In contrast, algorithmic rule requires that we use any informative quantifier decoration. We use this flexibility when giving guidelines for the required annotations in Section 3.1 for typing external terms, as the required conditions for typeability in Theorem 3.3 would be further complicated if we could not restrict ourselves to using only synthetic type-argument inference.
5. Discussion & Related Work
5.1. Local Type Inference and System F
Local Type Inference
Our work is most influenced by the seminal paper by Pierce and Turner(Pierce and Turner, 2000) on local type inference that describes its broad approach, including the two techniques of bidirectional typing rules and local type-argument inference and the design-space restriction that polymorphic function applications be fully-uncurried to maximize the benefit of these techniques. In their system, either all term arguments to polymorphic functions must be synthesized or else all type arguments must be given – no compromise is available when only a few type arguments suffice to type an application, be they provided explicitly or inferred contextually. Our primary motivation in this work was addressing these issues – restoring first-class currying, enabling partial type application, and utilizing the contextual type of an application for type-argument inference – while maintaining some of the desirable properties of local type inference and staying in the spirit of their approach.
Colored Local Type Inference
Odersky, Zenger, and Zenger(Odersky et al., 2001) improve upon the type system of Pierce and Turner by extending it to allow partial type information to be propagated downwards when inferring types for term arguments. Their insight was to internalize the two modes of bidirectional type inference to the structure of types themselves, allowing different parts of a type to be synthetic or contextual. In contrast, we use an “all or nothing” approach to type propagation: when we encountered a term argument for which we have incomplete information, we require that it fully synthesize its type. On the other hand, their system uses only the typing information provided by the application head, whereas we combine this with the contextual type of an application, allowing us to type some expressions their system cannot. The upshot of the difference in these systems is that spine-local type inference utilizes more contextual information and colored local type inference utilizes contextual information more cleverly.
The syntax for prototypes in our algorithm was directly inspired by the prototypes used in the algorithmic inference rules for (Odersky et al., 2001). Our use of prototypes complements theirs; ours propagates the partial type information provided by contextual type of an application spine to its head, whereas theirs propagates the partial type information provided by an application head to its arguments. In future work, we hope to combine these two notions of prototype to propagate partially the type information coming from the application’s contextual type and head to its arguments.
Local type inference is usually studied in the setting of System F which combines impredicative parametric polymorphism and subtyping. The reason for this is two-fold: first, a partial type inference technique is needed as complete type inference for F is undecidable(Tiuryn and Urzyczyn, 1996); second, global type inference systems fail to infer principal types in F (Odersky, 2002; Odersky et al., 1999; Kennedy, 1996), whereas local type inference is able to promise that it infers the “locally best”(Pierce and Turner, 2000) type arguments (i.e. the type arguments minimizing the result type of the application, relative to the subtyping relation). The setting for our algorithm is System F, so the reader may ask whether our developments can be extended gracefully to handle subtyping. We believe the answer is yes, though with some modification on how synthetic type arguments are used.