Every time you write code, your compiler quietly decides which types go where. In some languages you annotate everything; in others, type inference figures it out automatically. Bidirectional type checking lands squarely in the middle — and it does so deliberately.
The idea, formalized by Benjamin Pierce and David Turner in 2000, is to split every expression into one of two modes:
- Check mode (): the compiler already knows what type to expect, and it just checks that the expression agrees.
- Synthesis mode (): the compiler doesn't know the type yet, and it synthesizes (computes) it from the structure of .
The key insight is that these two modes flow into each other. A function application can synthesize the return type once it knows the function's type. A lambda body can be checked against the expected return type once an outer annotation provides it. Information flows inward when checking and outward when synthesizing — hence bidirectional.
The practical payoff is minimal annotations. You only need to write a type where the algorithm cannot synthesize one — typically at the top of a definition or when introducing a polymorphic function. Everything below that anchor inherits context and checks itself. Languages like Haskell, Rust, Scala, and many proof assistants rely on bidirectional ideas at their core.
Comments
Loading comments...