Let Arguments Go First
Let Arguments Go First
1 Introduction
Bi-directional type checking has been known in the folklore of type systems
for a long time. It was popularized by Pierce and Turner’s work on local type
inference [29]. Local type inference was introduced as an alternative to Hindley-
Milner (henceforth HM system) type systems [11, 17], which could easily deal
with polymorphic languages with subtyping. Bi-directional type checking is one
component of local type inference that, aided by some type annotations, en-
ables type inference in an expressive language with polymorphism and subtyp-
ing. Since Pierce and Turner’s work, various other authors have proved the ef-
fectiveness of bi-directional type checking in several other settings, including
many different systems with subtyping [12, 15, 14], systems with dependent
types [37, 10, 2, 21, 3], and various other works [1, 13, 28, 7, 22]. Furthermore,
bi-directional type checking has also been combined with HM-style techniques
for providing type inference in the presence of higher-ranked types [27, 14].
The key idea in bi-directional type checking is simple. In its basic form typing
is split into inference and checked modes. The most salient feature of a bi-
directional type-checker is when information deduced from inference mode is
used to guide checking of an expression in checked mode. One of such interactions
between modes happens in the typing rule for function applications:
Γ ` e1 ⇒ A → B Γ ` e2 ⇐ A
APP
Γ ` e1 e2 ⇒ B
2
In the above rule, which is a standard bi-directional rule for checking applica-
tions, the two modes are used. First we synthesize (⇒) the type A → B from e1 ,
and then check (⇐) e2 against A, returning B as the type for the application.
This paper presents a variant of bi-directional type checking that employs a
so-called application mode. With the application mode the design of the appli-
cation rule (for a simply typed calculus) is as follows:
Γ ` e2 ⇒ A Γ p Ψ, A ` e1 ⇒ A → B
APP
Γ p Ψ ` e1 e2 ⇒ B
In this rule, there are two kinds of judgments. The first judgment is just the
usual inference mode, which is used to infer the type of the argument e2 . The
second judgment, the application mode, is similar to the inference mode, but it
has an additional context Ψ . The context Ψ is a stack that tracks the types of
the arguments of outer applications. In the rule for application, the type of the
argument e2 is inferred first, and then pushed into Ψ for inferring the type of e1 .
Applications are themselves in the application mode, since they can be in the
context of an outer application. With the application mode it is possible to infer
the type for expressions such as (λx. x) 1 without additional annotations.
Bi-directional type checking with an application mode may still require type
annotations and it gives different trade-offs with respect to the checked mode
in terms of type annotations. However the different trade-offs open paths to
different designs of type checking/inference algorithms. To illustrate the utility
of the application mode, we present two different calculi as applications. The
first calculus is a higher ranked implicit polymorphic type system, which infers
higher-ranked types, generalizes the HM type system, and has polymorphic let
as syntactic sugar. As far as we are aware, no previous work enables an HM-style
let construct to be expressed as syntactic sugar. For this calculus many results
are proved using the Coq proof assistant [9], including type-safety. Moreover a
sound and complete algorithmic system, inspired by Peyton Jones et al. [27],
is also developed. A second calculus with explicit polymorphism illustrates how
the application mode is compatible with type applications, and how it adds
expressiveness by enabling an encoding of type declarations in a System-F-like
calculus. For this calculus, all proofs (including type soundness), are mechanized
in Coq.
We believe that, similarly to standard bi-directional type checking, bi-directional
type checking with an application mode can be applied to a wide range of type
systems. Our work shows two particular and non-trivial applications. Other po-
tential areas of applications are other type systems with subtyping, static over-
loading, implicit parameters or dependent types.
In summary the contributions of this paper are1 :
– A variant of bi-directional type checking where the inference mode is
combined with a new, so-called, application mode. The application mode
naturally propagates type information from arguments to the functions.
1
All supplementary materials are available in https://bitbucket.org/ningningxie/
let-arguments-go-first
3
2 Overview
2.1 Background: Bi-Directional Type Checking
Traditional type checking rules can be heavyweight on annotations, in the sense
that lambda-bound variables always need explicit annotations. Bi-directional
type checking [29] provides an alternative, which allows types to propagate down-
ward the syntax tree. For example, in the expression (λf:Int → Int. f) (λy.
y), the type of y is provided by the type annotation on f. This is supported by
the bi-directional typing rule for applications:
Γ ` e1 ⇒ A → B Γ ` e2 ⇐ A
APP
Γ ` e1 e2 ⇒ B
Specifically, if we know that the type of e1 is a function from A → B, we can check
that e2 has type A. Notice that here the type information flows from functions
to arguments.
One guideline for designing bi-directional type checking rules [15] is to dis-
tinguish introduction rules from elimination rules. Constructs which correspond
to introduction forms are checked against a given type, while constructs cor-
responding to elimination forms infer (or synthesize) their types. For instance,
under this design principle, the introduction rule for pairs is supposed to be in
checked mode, as in the rule Pair-C.
Γ ` e1 ⇐ A Γ ` e2 ⇐ B Γ ` e1 ⇒ A Γ ` e2 ⇒ B
Pair-C Pair-I
Γ ` (e1 , e2 ) ⇐ (A, B) Γ ` (e1 , e2 ) ⇒ (A, B)
Unfortunately, this means that the trivial program (1, 2) cannot type-check,
which in this case has to be rewritten to (1, 2) : (Int , Int).
In this particular case, bi-directional type checking goes against its original
intention of removing burden from programmers, since a seemingly unnecessary
annotation is needed. Therefore, in practice, bi-directional type systems do not
strictly follow the guideline, and usually have additional inference rules for the
introduction form of constructs. For pairs, the corresponding rule is Pair-I.
Now we can type check (1, 2), but the price to pay is that two typing rules
for pairs are needed. Worse still, the same criticism applies to other constructs.
4
This shows one drawback of bi-directional type checking: often to minimize anno-
tations, many rules are duplicated for having both inference and checked mode,
which scales up with the typing rules in a type system.
The Application Mode. If types flow from the arguments to the function, an
alternative idea is to push the type of the arguments into the typing of the
function, as the rule that is briefly introduced in Section 1:
Γ ` e2 ⇒ A Γ p Ψ, A ` e1 ⇒ A → B
APP
Γ p Ψ ` e1 e2 ⇒ B
Here the argument e2 synthesizes its type A, which then is pushed into the
application context Ψ . Lambda expressions can now make use of the application
context, leading to the following rule:
Γ, x : A p Ψ ` e ⇒ B
Lam
Γ p Ψ, A ` λx. e ⇒ A → B
The type A that appears last in the application context serves as the type for x,
and type checking continues with a smaller application context and x:A in the
typing context. Therefore, using the rule App and Lam, the expression (λx.
x) 1 can type-check without annotations, since the type Int of the argument 1
is used as the type of the binding x.
5
Note that, since the examples so far are based on simple types, obviously
they can be solved by integrating type inference and relying on techniques like
unification or constraint solving. However, here the point is that the application
mode helps to reduce the number of annotations without requiring such sophis-
ticated techniques. Also, the application mode helps with situations where those
techniques cannot be easily applied, such as type systems with subtyping.
Interpretation of the Application Mode. As we have seen, the guideline for de-
signing bi-directional type checking [15], based on introduction and elimination
rules, is often not enough in practice. This leads to extra introduction rules in
the inference mode. The application mode does not distinguish between intro-
duction rules and elimination rules. Instead, to decide whether a rule should be
in inference or application mode, we need to think whether the expression can be
applied or not. Variables, lambda expressions and applications are all examples
of expressions that can be applied, and they should have application mode rules.
However pairs or literals cannot be applied and should have inference rules. For
example, type checking pairs would simply lead to the rule Pair-I. Nevertheless
elimination rules of pairs could have non-empty application contexts (see Sec-
tion 5.2 for details). In the application mode, arguments are always inferred first
in applications and propagated through application contexts. An empty appli-
cation context means that an expression is not being applied to anything, which
allows us to model the inference mode as a particular case2 .
Partial Type Checking. The inference mode synthesizes the type of an expression,
and the checked mode checks an expression against some type. A natural question
is how do these modes compare to application mode. An answer is that, in some
sense: the application mode is stronger than inference mode, but weaker than
checked mode. Specifically, the inference mode means that we know nothing
about the type an expression before hand. The checked mode means that the
whole type of the expression is already known before hand. With the application
mode we know some partial type information about the type of an expression:
we know some of its argument types (since it must be a function type when the
application context is non-empty), but not the return type.
Instead of nothing or all, this partialness gives us a finer grain notion on
how much we know about the type of an expression. For example, assume
e : A → B → C. In the inference mode, we only have e. In the checked mode, we
have both e and A → B → C. In the application mode, we have e, and maybe
an empty context (which degenerates into inference mode), or an application
context A (we know the type of first argument), or an application context B, A
(we know the types of both arguments).
Trade-offs. Note that the application mode is not conservative over traditional
bidirectional type checking due to the different information flow. However, it
2
Although the application mode generalizes the inference mode, we refer to them as
two different modes. Thus the variant of bi-directional type checking in this paper
is interpreted as a type system with both inference and application modes.
6
Local Constraint Solver for Function Variables. Many type systems, including
type systems with implicit polymorphism and/or static overloading, need infor-
mation about the types of the arguments when type checking function variables.
For example, in conventional functional languages with implicit polymorphism,
function calls such as (id 3) where id: ∀a. (a → a), are pervasive. In such a
function call the type system must instantiate a to Int. Dealing with such im-
plicit instantiation gets trickier in systems with higher-ranked types. For example,
Peyton Jones et al. [27] require additional syntactic forms and relations, whereas
Dunfield and Krishnaswami [14] add a special purpose application judgment.
With the application mode, all the type information about the arguments be-
ing applied is available in application contexts and can be used to solve instanti-
ation constraints. To exploit such information, the type system employs a special
subtyping judgment called application subtyping, with the form Ψ ` A ≤ B. Un-
like conventional subtyping, computationally Ψ and A are interpreted as inputs
and B as output. In above example, we have that Int ` ∀a.a → a ≤ B and we
can determine that a = Int and B = Int → Int. In this way, type system is able
to solve the constraints locally according to the application contexts since we no
longer need to propagate the instantiation constraints to the typing process.
Such syntactic sugar for let is, of course, standard. However, in the context of
implementations of typed languages it normally requires extra type annotations
or a more sophisticated type-directed translation. Type checking (λx. e2 ) e1
would normally require annotations (for example an annotation for x), or other-
wise such annotation should be inferred first. Nevertheless, with the application
mode no extra annotations/inference is required, since from the type of the ar-
gument e1 it is possible to deduce the type of x. Generally speaking, with the
application mode annotations are never needed for applied lambdas. Thus let
can be the usual sugar from the untyped lambda calculus, including HM-style
let expression and even type declarations.
Higher-ranked Types. Type systems with higher-ranked types generalize the tra-
ditional HM type system, and are useful in practice in languages like Haskell or
other ML-like languages. Essentially higher-ranked types enable much of the ex-
pressive power of System F, with the advantage of implicit polymorphism. Com-
plete type inference for System F is known to be undecidable [36]. Therefore,
several partial type inference algorithms, exploiting additional type annotations,
have been proposed in the past instead [25, 15, 31, 27].
Type Inference for Higher-ranked Types with the Application Mode. Using our
bi-directional type system with an application mode, the original expression can
type check without annotations or rewrites: (λf. (f 1, f ’c’)) (λx. x).
This result comes naturally if we allow type information flow from arguments
to functions. For inferring polymorphic types for arguments, we use generaliza-
tion. In the above example, we first infer the type ∀a. a → a for the argument,
then pass the type to the function. A nice consequence of such an approach
is that HM-style polymorphic let expressions are simply regarded as syntactic
sugar to a combination of lambda/application:
let x = e1 in e2 (λx. e2 ) e1
With this approach, nested lets can lead to types which are more general than
HM. For example, let s = λx. x in let t = λy. s in e. The type of s is ∀a.
a → a after generalization. Because t returns s as a result, we might expect
8
t: ∀b. b → (∀a. a → a), which is what our system will return. However, HM
will return type t: ∀b. ∀a. b → (a → a), as it can only return rank 1 types,
which is less general than the previous one according to Odersky and Läufer’s
subtyping relation for polymorphic types [24].
Conservativity over the Hindley-Milner Type System. Our type system is a con-
servative extension over the Hindley-Milner type system, in the sense that every
program that can type-check in HM is accepted in our type system, which is
explained in detail in Section 3.2. This result is not surprising: after desugaring
let into a lambda and an application, programs remain typeable.
binders can be omitted in their system. This requires understanding how the
applications in checked mode operate.
In our system the above expression is not typeable, as a consequence of
the information flow in the application mode. However, following our guideline
for annotations leads to a program that can be type-checked with a smaller
annotation: (λf. f) (λg : (∀a. a → a). (g 1, g ’a’)). This means that our
work is not conservative over their work, which is due to the design choice of the
application typing rule. Nevertheless, we can always rewrite programs using our
guideline, which often leads to fewer/smaller annotations.
Sugar for Type Synonyms. In the same way that we can regard let expressions
as syntactic sugar, in the new type system we further gain built-in type synonyms
for free. A type synonym is a new name for an existing type. Type synonyms
are common in languages such as Haskell. In our calculus a simple form of type
synonyms can be desugared as follows:
10
type a = A in e (Λa. e) A
One practical benefit of such syntactic sugar is that it enables a direct en-
coding of a System F-like language with declarations (including type-synonyms).
Although declarations are often viewed as a routine extension to a calculus, and
are not formally studied, they are highly relevant in practice. Therefore, a more
realistic formalization of a programming language should directly account for
declarations. By providing a way to encode declarations, our new calculus en-
ables a simple way to formalize declarations.
Type Abstraction. The type equalities introduced by type applications may seem
like we are breaking System F type abstraction. However, we argue that type
abstraction is still supported by our System F variant. For example:
let inc = Λa. λx : a. x + 1 in inc Int e
(after desugaring) does not type-check, as in a System-F like language. In our
type system lambda abstractions that are immediatelly applied to an argument,
and unapplied lambda abstractions behave differently. Unapplied lambda ab-
stractions are just like System F abstractions and retain type abstraction. The
example above illustrates this. In contrast the typeable example (Λa. λx : a.
x + 1) Int, which uses a lambda abstraction directly applied to an argument,
can be regarded as the desugared expression for type a = Int in λx : a . x + 1.
3.1 Syntax
The syntax of the language is:
Expr e ::= x | n | λx : A. e | λx. e | e1 e2
Type A, B ::= a | A → B | ∀a.A | Int
Monotype τ ::= a | τ1 → τ2 | Int
Typing Context Γ ::= ∅ | Γ, x : A
Application Context Ψ ::= ∅ | Ψ, A
Types. Types include type variables (a), functions (A → B), polymorphic types
(∀a.A) and integers (Int). We use capital letters (A, B) for types, and small letters
(a, b) for type variables. Monotypes are types without universal quantifiers.
Contexts. Typing contexts Γ are standard: they map a term variable x to its
type A. We implicitly assume that all the variables in Γ are distinct. The main
novelty lies in the application contexts Ψ , which are the main data structure
needed to allow types to flow from arguments to functions. Application contexts
are modeled as a stack. The stack collects the types of arguments in applications.
The context is a stack because if a type is pushed last then it will be popped first.
For example, inferring expression e under application context (a, Int), means e
is now being applied to two arguments e1 , e2 , with e1 : Int, e2 : a, so e should be
of type Int → a → A for some A.
Γ pΨ `e ⇒ B
x:A∈Γ Ψ ` A <: B
T-Var T-Int
Γp Ψ ` x ⇒ B Γ ` n ⇒ Int
Γ, x : A p Ψ ` e ⇒ B Γ, x : τ ` e ⇒ B
T-Lam T-Lam2
Γ p Ψ, A ` λx. e ⇒ A → B Γ ` λx. e ⇒ τ → B
Γ, x : A ` e ⇒ B
T-LamAnn1
Γ ` λx : A. e ⇒ A → B
Γ ` e2 ⇒ A Γgen (A) = B Γ p Ψ, B ` e1 ⇒ B → C
T-App
Γ p Ψ ` e1 e2 ⇒ C
A <: B
A <: B
S-Int S-Var S-ForallR
Int <: Int a <: a A <: ∀a.B
Ψ, C ` AJa 7→ τ K <: B
S-Empty S-ForallL2
∅ ` A <: A Ψ, C ` ∀a.A <: B
C <: A Ψ ` B <: D
S-Fun2
Ψ, C ` A → B <: C → D
the type of e1 is now inferred under an application context extended with type
B. The generalization step is important to infer higher ranked types: since B
is a possibly polymorphic type, which is the argument type of e1 , then e1 is of
possibly a higher rank type.
Let Expressions. The language does not have built-in let expressions, but in-
stead supports let as syntactic sugar. The typing rule for let expressions in the
HM system is (without the gray-shaded part):
Γ ` e 1 ⇒ A1 Γgen (A1 ) = A2 Γ, x : A2 p Ψ ` e2 ⇒ B
T-Let
Γ p Ψ ` let x = e1 in e2 ⇒ B
13
Γ, x : A2 p Ψ ` e2 ⇒ B
T-Lam
Γ ` e 1 ⇒ A1 Γgen (A1 ) = A2 Γ p Ψ, A2 ` λx. e2 ⇒ A2 → B
T-App
Γ p Ψ ` (λx. e2 ) e1 ⇒ B
The type A2 is now pushed into application context in rule T-App, and then
assigned to x in T-Lam. Comparing this with the typing derivations with rule
T-Let, we now have same preconditions. Thus we can see that the rules in
Figure 1 are sufficient to express an HM-style polymorphic let construct.
Such definition is useful to reason about the typing result with application
contexts. One specific property is that the application context determines the
form of the typing result.
The relationship between our system and standard Hindley Milner type sys-
tem can be established through the desugaring of let expressions. Namely, if e is
typeable in Hindley Milner system, then the desugared expression |e| is typeable
in our system, with a more general typing result.
Lemma 3 (Conservative over HM). If Γ `HM e ⇒ A, then for some B,
we have Γ ` |e| ⇒ B, and B <: A.
14
3.3 Subtyping
We present our subtyping rules at the bottom of Figure 1. Interestingly, our
subtyping has two different forms.
Subtyping. The first judgment follows Odersky and Läufer [24]. A <: B means
that A is more polymorphic than B and, equivalently, A is a subtype of B. Rules
S-Int and S-Var are trivial. Rule S-ForallR states A is subtype of ∀a.B only
if A is a subtype of B, with the assumption a is a fresh variable. Rule S-ForallL
says ∀a.A is a subtype of B if we can instantiate it with some τ and show the
result is a subtype of B. In rule S-Fun, we see that subtyping is contra-variant
on the argument type, and covariant on the return type.
Application Subtyping. The typing rule T-Var uses the second subtyping judg-
ment Ψ ` A <: B. To motivate this new kind of judgment, consider the ex-
pression id 1 for example, whose derivation is stuck at T-Var (here we assume
id : ∀a.a → a ∈ Γ ):
id : ∀a.a → a ∈ Γ ???
T-Var
Γ ` 1 ⇒ Int Γgen (Int) = Int Γ p Int ` id ⇒
T-App
Γ ` id 1 ⇒
Here we know that id : ∀a.a → a and also, from the application context, that
id is applied to an argument of type Int. Thus we need a mechanism for solving
the instantiation a = Int and return a supertype Int → Int as the type of id. This
is precisely what the application subtyping achieves: resolve instantiation con-
straints according to the application context. Notice that unlike existing works
[27, 14], application subtyping provides a way to solve instantiation more locally,
since it does not mutually depend on typing.
Back to the rules in Figure 1, one way to understand the judgment Ψ `
A <: B from a computational point-of-view is that the type B is a computed
output, rather than an input. In other words B is determined from Ψ and A. This
is unlike the judgment A <: B, where both A and B would be computationally
interpreted as inputs. Therefore it is not possible to view A <: B as a special
case of Ψ ` A <: B where Ψ is empty.
There are three rules dealing with application contexts. Rule S-Empty is
for case when the application context is empty. Because it is empty, we have no
constraints on the type, so we return it back unchanged. Note that this is where
HM systems (also Peyton Jones et al. [27]) would normally use a rule Inst to
remove top-level quantifiers:
Inst
∀a.A <: AJa 7→ τ K
Our system does not need Inst, because in applications, type information flows
from arguments to the function, instead of function to arguments. In the latter
case, Inst is needed because a function type is wanted instead of a polymorphic
type. In our approach, instantiation of type variables is avoided unless necessary.
15
The two remaining rules apply when the application context is non-empty,
for polymorphic and function types respectively. Note that we only need to
deal with these two cases because Int or type variables a cannot have a non-
empty application context. In rule S-Forall2, we instantiate the polymorphic
type with some τ , and continue. This instantiation is forced by the application
context. In rule S-Fun2, one function of type A → B is now being applied to an
argument of type C. So we check C <: A. Then we continue with B and the
rest application context, and return C → D as the result type of the function.
(S0 , N0 ) p Γ, x : βb ` e ⇒ A ,→ (S1 , N1 )
AT-Lam1
b p Γ ` λx. e ⇒ βb → A ,→ (S1 , N1 )
(S0 , N0 β)
17
4.1 Syntax
We focus on a new variant of the standard System F. The syntax is as follows:
Applying Contexts. The typing contexts contain type equations, which can be
used as substitutions. For example, a = Int, x : Int, b = Bool can be applied to
a → b to get the function type Int → Bool . We write hΓ iA for Γ applied as a
substitution to type A. The formal definition is given in Figure 2.
18
a∈Γ Γ `A Γ `B Γ, a ` A
WF-TVar WF-Int WF-Arrow WF-All
Γ `a Γ ` Int Γ `A→B Γ ` ∀a.A
Fig. 3. Well-formedness.
Typing Judgments. From Lemma 1 and Lemma 4, we know that the application
context always coincides with typing/subtyping results. This means that the
types of the arguments can be recovered from the application context. So instead
of the whole type, we can use only the return type as the output type. For
example, we review the rule T-Lam in Figure 1:
Γ, x : A p Ψ ` e ⇒ B Γ, x : A p Ψ ` e ⇒ C
T-Lam T-Lam-Alt
Γ p Ψ, A ` λx. e ⇒ A → B Γ p Ψ, A ` λx. e ⇒ C
Typing Rules. Using the new interpretation of the typing judgment, we give the
typing rules in the top of Figure 4. SF-Var depends on the subtyping rules.
19
Γ pΨ `e ⇒ B
Γ, x : hΓ iA ` e ⇒ B
SF-LamAnn1
Γ ` λx : A. e ⇒ hΓ iA → B
Γ, x : hΓ iA p Ψ ` e ⇒ B Γ, x : A p Ψ ` e ⇒ B
SF-LamAnn2 SF-Lam
Γ p Ψ, hΓ iA ` λx : A. e ⇒ B Γ p Ψ, A ` λx. e ⇒ B
Γ ` e2 ⇒ A Γ p Ψ, A ` e1 ⇒ B Γ, a ` e ⇒ B
SF-App SF-TLam1
Γ p Ψ ` e1 e2 ⇒ B Γ ` Λa.e ⇒ ∀a.B
Γ, a = A p Ψ ` e ⇒ B Γ p Ψ, [hΓ iA] ` e ⇒ B
SF-TLam2 SF-TApp
Γ p Ψ, [A] ` Λa.e ⇒ B Γ p Ψ ` e [A] ⇒ B
Ψ ` A <: B
SF-SEmpty
∅ ` A <: A
Rule SF-Int always infers integer types. Rule SF-LamAnn1 first applies cur-
rent context on A, then puts x : hΓ iA into the typing context to infer e. The
return type is a function type because the application context is empty. Rule
SF-LamAnn2 has a non-empty application context, so it requests that the type
at the top of the application context is equivalent to hΓ iA. The output type
is B instead of a function type. Notice how the invariant that types are fully
substituted under the typing context is preserved in these two rules.
Rule SF-Lam pops the type A from the application context, puts x : A into
the typing context, and returns only the return type B. In rule SF-App, the
argument type A is pushed into the application context for inferring e1 , so the
output type B is the type of e1 under application context (Ψ, A), which is exactly
the return type of e1 e2 under Ψ .
Rule SF-TLam1 is for type abstractions. The type variable a is pushed
into the typing context, and the return type is a polymorphic type. In rule SF-
TLam2, the application context has the type argument A at its top, which means
the type abstraction is applied to A. We then put the type equation a = A into
the typing context to infer e. Like term-level applications, here we only return
the type B instead of a polymorphic type. In rule SF-TApp, we first apply the
20
typing context on the type argument A, then we put the applied type argument
hΓ iA into the application context to infer e, and return B as the output type.
5 Discussion
This section discusses possible design choices regarding bi-directional type check-
ing with the application mode, and talks about possible future work.
21
Ψ ` A <: B Γ `e ⇐ A
T-Ann
Γ p Ψ ` (e : A) ⇒ B
Γ,x : A ` e ⇐ B Γ ` e2 ⇒ A Γ ` e1 ⇐ A → B
Abs-Chk App-Chk
Γ ` λx. e ⇐ A → B Γ ` e1 e2 ⇐ B
Note that adding expression annotations might bring convenience for pro-
grammers, since annotations can be more freely placed in a program. For exam-
ple, (λf. f 1) : (Int → Int) → Int becomes valid. However this does not add
expressive power, since programs that are typeable under expression annotations,
would remain typeable after moving the annotations to bindings. For example
the previous program is equivalent to (λf : (Int → Int). f 1).
This discussion is a sketch. We have not defined the corresponding declarative
system nor algorithm. However we believe that the addition of a checked mode
will not bring surprises to the meta-theory.
Γ ` e1 ⇒ A Γ ` e2 ⇒ B A1 <: B1 A2 <: B2
T-Pair S-Pair
Γ ` (e1 , e2 ) ⇒ (A, B) (A1 , A2 ) <: (B1 , B2 )
22
The application mode can apply to the elimination constructs of pairs. If one
component of the pair is a function, for example, (fst (λx. x, 3) 4), then it is
possible to have a judgment with a non-empty application context. Therefore,
we can use the application subtyping to account for the application contexts:
Note that another way to model those two rules would be to simply have an
initial typing environment Γinitial ≡ fst : (∀ab.(a, b) → a), snd : (∀ab.(a, b) → b).
In this case the elimination of pairs be dealt directly by the rule for variables.
An extended version of the calculus presented in Section 3, which includes
the rules for pairs (T-Pair, S-Pair, T-Fst2 and T-Snd2), has been formally
studied. All the theorems presented in Section 3 hold with the extension of pairs.
One remark about the application mode is that the same idea is possibly appli-
cable to systems with advanced features, where type inference is sophisticated
or even undecidable. One promising application is, for instance, dependent type
systems [37, 10, 2, 21, 3]. Type systems with dependent types usually unify the
syntax for terms and types, with a single lambda abstraction generalizing both
type and lambda abstractions. Unfortunately, this means that the let desugar
is not valid in those systems. As a concrete example, consider desugaring the
expression let a = Int in λx : a. x + 1 into (λa. λx : a. x + 1) Int, which is ill-
typed because the type of x in the abstraction body is a and not Int.
Because let cannot be encoded, declarations cannot be encoded either. Mod-
eling declarations in dependently typed languages is a subtle matter, and nor-
mally requires some additional complexity [34].
We believe that the same technique presented in Section 4 can be adapted
into a dependently typed language to enable a let encoding. In a dependent type
system with unified syntax for terms and types, we can combine the two forms
23
6 Related Work
6.1 Bi-Directional Type Checking
Bi-directional type checking was popularized by the work of Pierce and Turner
[29]. It has since been applied to many type systems with advanced features. The
alternative application mode introduced by us enables a variant of bi-directional
type checking. There are many other efforts to refine bi-directional type checking.
Colored local type inference [25] refines local type inference for explicit poly-
morphism by propagating partial type information. Their work is built on dis-
tinguishing inherited types (known from the context) and synthesized types (in-
ferred from terms). A similar distinction is achieved in our algorithm by ma-
nipulating type variables [14]. Also, their information flow is from functions to
arguments, which is fundamentally different from the application mode.
The system of tridirectional type checking [15] is based on bi-directional type
checking and has a rich set of property types including intersections, unions and
quantified dependent types, but without parametric polymorphism. Tridirec-
tional type checking has a new direction for supporting type checking unions
and existential quantification. Their third mode is basically unrelated to our
application mode, which propagates information from outer applications.
Greedy bi-directional polymorphism [13] adopts a greedy idea from Cardelli
[4] on bi-directional type checking with higher ranked types, where the type
variables in instantiations are determined by the first constraint. In this way,
they support some uses of impredicative polymorphism. However, the greediness
also makes many obvious programs rejected.
Predicative Systems. Peyton Jones et al. [27] developed an approach for type in-
ference for higher rank types using traditional bi-directional type checking based
on Odersky and Läufer [24]. However in their system, in order to do instantia-
tion on higher rank types, they are forced to have an additional type category (ρ
24
types) as a special kind of higher rank type without top-level quantifiers. This
complicates their system since they need to have additional rule sets for such
types. They also combine a variant of the containment relation from Mitchell
[23] for deep skolemisation in subsumption rules, which we believe is compatible
with our subtyping definition.
Dunfield and Krishnaswami [14] build a simple and concise algorithm for
higher ranked polymorphism based on traditional bidirectional type checking.
They deal with the same language of Peyton Jones et al. [27], except they do
not have let expressions nor generalization (though it is discussed in design
variations). They have a special application judgment which delays instantiation
until the expression is applied to some argument. As with application mode, this
avoids the additional category of types. Unlike their work, our work supports
generalization and HM-style let expressions. Moreover the use of an application
mode in our work introduces several differences as to when and where annota-
tions are needed (see Section 2.4 for related discussion).
Impredicative Systems. MLF [18, 32, 19] generalizes ML with first-class poly-
morphism. MLF introduces a new type of bounded quantification (either rigid
or flexible) for polymorphic types so that instantiation of polymorphic bindings
is delayed until a principal type is found. The HML system [20] is proposed as
a simplification and restriction of MLF . HML only uses flexible types, which
simplifies the type inference algorithm, but retains many interesting properties
and features.
The FPH system [35] introduces boxy monotypes into System F types. One
critique of boxy type inference is that the impredicativity is deeply hidden in the
algorithmic type inference rules, which makes it hard to understand the interac-
tion between its predicative constraints and impredicative instantiations [31].
7 Conclusion
We proposed a variant of bi-directional type checking with a new application
mode, where type information flows from arguments to functions in applications.
The application mode is essentially a generalization of the inference mode, can
therefore work naturally with inference mode, and avoid the rule duplication
that is often needed in traditional bi-directional type checking. The application
mode can also be combined with the checked mode, but this often does not
add expressiveness. Compared to traditional bi-directional type checking, the
application mode opens a new path to the design of type inference/checking.
We have adopted the application mode in two type systems. Those two sys-
tems enjoy many interesting properties and features. However as bi-directional
type checking can be applied to many type systems, we believe application mode
is applicable to various type systems. One obvious potential future work is to
investigate more systems where the application mode brings benefits. This in-
cludes systems with subtyping, intersection types [30, 8], static overloading, or
dependent types.
Acknowledgements
We thank the anonymous reviewers for their helpful comments. This work has
been sponsored by the Hong Kong Research Grant Council projects number
17210617 and 17258816.
Bibliography
[20] Daan Leijen. Flexible types: Robust type inference for first-class polymor-
phism. POPL ’09, 2009.
[21] Andres Löh, Conor McBride, and Wouter Swierstra. A tutorial implemen-
tation of a dependently typed lambda calculus. Fundamenta informaticae,
102(2):177–207, 2010.
[22] William Lovas. Refinement Types for Logical Frameworks. PhD thesis,
Carnegie Mellon University, 2010. AAI3456011.
[23] John C Mitchell. Polymorphic type inference and containment. Information
and Computation, 76(2-3):211–249, 1988.
[24] Martin Odersky and Konstantin Läufer. Putting type annotations to work.
POPL ’96, 1996.
[25] Martin Odersky, Christoph Zenger, and Matthias Zenger. Colored local
type inference. POPL ’01, 2001.
[26] Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, and Geoffrey
Washburn. Simple unification-based type inference for gadts. ICFP ’06,
2006.
[27] Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, and Mark
Shields. Practical type inference for arbitrary-rank types. Journal of func-
tional programming, 17(01):1–82, 2007.
[28] Brigitte Pientka. A type-theoretic foundation for programming with higher-
order abstract syntax and first-class substitutions. POPL ’08, 2008.
[29] Benjamin C Pierce and David N Turner. Local type inference. TOPLAS,
22(1):1–44, 2000.
[30] Garrel Pottinger. A type assignment for the strongly normalizable λ-terms.
To HB Curry: essays on combinatory logic, lambda calculus and formalism,
pages 561–577, 1980.
[31] Didier Rémy. Simple, partial type-inference for system f based on type-
containment. ICFP ’05, 2005.
[32] Didier Rémy and Boris Yakobowski. From ml to mlf: Graphic type con-
straints with efficient type inference. ICFP ’08, 2008.
[33] Tom Schrijvers, Simon Peyton Jones, Martin Sulzmann, and Dimitrios Vy-
tiniotis. Complete and decidable type inference for gadts. ICFP ’09, 2009.
[34] Paula Severi and Erik Poll. Pure type systems with definitions. Logical
Foundations of Computer Science, pages 316–328, 1994.
[35] Dimitrios Vytiniotis, Stephanie Weirich, and Simon Peyton Jones. Fph:
First-class polymorphism for haskell. ICFP ’08, 2008.
[36] Joe B Wells. Typability and type checking in system f are equivalent and
undecidable. Annals of Pure and Applied Logic, 98(1-3):111–156, 1999.
[37] Hongwei Xi and Frank Pfenning. Dependent types in practical program-
ming. POPL ’99, 1999.