In HM type inference, integer constants always have type int and generate no constraints, boolean constants always have type bool and generate no constraints, and names are inferred by looking up their type in the static environment (which must contain bindings for built-in operators like +, *, and <= with type int arrow int arrow int); if a name is not bound in the environment, type inference fails.
HM Type Inference: Constants and Names | OCaml Programming
Added:Basic familiarity with OCaml syntax, particularly how variables (names) and literals (constants) are declared and used.

In OCaml, variables are declared using the 'let' keyword followed by the variable name and an equals sign, then the value. For example, 'let x = 2' declares a variable x with the value 2. The interpreter immediately displays the value of the variable. Variables in OCaml are immutable by default, meaning their value cannot be changed after declaration. The interpreter shows the value of variables when they are referenced.

To declare a variable: write 'variable' followed by name and type separated by colon (e.g., 'variable X: entier'). The type indicates what values can be stored. To declare a constant: write 'constante' followed by name and value separated by equals sign (e.g., 'constante pi = 3.14'). Constants don't require type specification. Once assigned, constant values cannot be changed.

Variables in OCaml are containers that hold values, declared using the 'let' keyword with the syntax 'let variable = value', where the language automatically infers the variable's type (such as integer, string, or boolean); variables can be reassigned to new values, and when a variable is composed of other variables, it must be explicitly redeclared to reflect changes in those underlying variables.

Constants are fixed values declared with identifiers (e.g., const int beta = 200;). Variable declaration syntax: type identifier; Multiple variables of same type can share one declaration. Assignment operator '=' stores values in variables, distinct from mathematical equality. Variables can be initialized at declaration with values, using scientific notation for floats (e.g., 1.2e-3) and single quotes for characters.

OCaml is a functional language similar to Haskell but with imperative features. Basic operations work like other languages, but OCaml uses double semicolons (;;) to end lines. Variables are defined with 'let' because = tests equality, not assignment. 'let' expressions create local scope. Functions use 'let function_name (args) = body' syntax, with 'rec' for recursion. Conditional statements use 'if ... then ... else'. Logical operators include = for equality and <> for inequality. The compiler infers types automatically. Pattern matching is a generalized form of if statements with multiple branches using '|' for 'or' and '_' as wildcard. For single-argument functions, the 'function' keyword automatically triggers pattern matching.
The conceptual definition of static typing and the fundamental goal of type inference algorithms.

The fundamental basis for understanding type inference is the simple type system for lambda calculus without polymorphism. In this system, all expressions have principal type assignments, and the principal type assignment is the unique type that represents all possible type assignments for the expression. For example, for the expression (x y) where x and y are variables, if x has type t1 → t2 and y has type t1, then the principal type assignment is t2. The type inference algorithm for this system takes an expression as input and returns its principal type assignment. The correctness of this algorithm can be proven using the unification technique. This represents the foundation upon which more complex type inference systems are built.

Static typing is a system where the compiler verifies variable and data types at compile-time to ensure expected types are used. This verification occurs during compilation, providing security by catching type errors before execution. Static languages are more restrictive with decoding rules that cannot be broken, resulting in longer but more readable code requiring explicit type declarations. Type inference is a feature in some static languages where the compiler automatically determines variable types from initial assignments, though the inferred type remains fixed afterward. Examples include C#, Java, and Swift.

The type inference algorithm translates typing rules into data-driven constraints through three steps: numbering AST expressions with placeholders, generating constraints by walking the tree, and solving constraints to find valid type substitutions. Constraints capture relationships between types, such as requiring a function's input type to match an argument's type. Solving involves picking constraints, replacing type variables with matching types, and propagating substitutions throughout. Unification checks structural compatibility—function types require matching input and output types. Advanced topics include generalization (creating polymorphic types from monomorphic ones by introducing type parameters at unconstrained variables) and structural records (defining types by field structure rather than names, allowing incremental growth as more information becomes available).

Type inference is a technique used in statically typed programming languages to automatically determine the types of expressions without explicit annotations; the process involves three key steps: generating constraints from the program structure, solving these constraints through unification to find the most general type assignments, and annotating the program with the inferred types, with ML being a prominent example of a language supporting strong type inference capabilities.

Hindley-Milner type checking is a general algorithm that automatically infers types for all variables and expressions in a program. It uses base knowledge about constants (integers, reals, booleans, strings), operator constraints (relational operators require equal operands and produce Booleans; arithmetic operators require numeric types), array access rules (arrays require integer indices and return elements of the same type), and function application rules (functions must accept provided arguments and return expected types). The algorithm builds these constraints together and infers the most general types that satisfy all constraints, throwing type errors when contradictions arise.
The concept of a Type Environment (often denoted as Gamma) and how it maps identifiers to their respective types or type schemes.

A type environment (Γ) maps variables to their types. The fundamental judgment is Γ ⊢ M:τ, meaning under environment Γ, term M has type τ. STLC type systems are defined as natural deduction systems with rules having premises and conclusions. The Constants rule handles atomic constants, the Variable rule handles variables, the Abstraction rule introduces function types (λX:τ1.M:τ1 → τ2), the Application rule handles function application (M1 M2:τ2 when M1:τ1 → τ2 and M2:τ1), the Product rule constructs product types, and the Projection rule accesses components.

A type environment is a finite collection of variable-to-type bindings, where types are generated by a context grammar. Unlike untyped languages with infinite variables, type environments only contain program identifiers. Context updation (γ updated by Δ) creates a new context where defined variables in Δ take precedence over γ. This handles variable redefinition, where the most recent declaration takes effect. The defined variables of a context are those identifiers appearing as defined variables in its binding structure.

A type environment (gamma) maps variables to types. For a program to be well-typed, the type environment must agree with the memory store (sigma) on the types of all variables. This means that for every variable, the type assigned in gamma must match the type of the value stored in sigma.

The type scheme introduction rule states that if, given a type environment and type class environment, an expression e has type T and a is not a free type variable in the assumptions, then e has the type scheme 'for all a in gamma. T'. This rule allows the type system to generalize types by adding universal quantifiers when no assumptions have been made about the type variable a. The rule ensures that the generalized type scheme holds for all types satisfying the type class constraints.

Context (denoted by Greek letter Gamma) is a dictionary that maps variable names to their assigned types. It represents the current state of variable type information in the program. Context is used to track what types variables have at different points in the program.
An introductory understanding of type constraints and how equality between types is formulated as equations.

Complex type expressions can be treated as equations to solve. For example, determining the cardinality of Either Bool (Maybe Bool) → Bool involves substituting: Either has 2 + 1 = 3, Maybe has 1 + 2 = 3, so the function type has 2^3 = 8 inhabitants.

Type inference generates equations between types (e.g., T1 = int). The algorithm solves these equations by substituting known types. If the system of equations is inconsistent (e.g., T1 = int and T1 = bool), the type inference fails and a type error is reported.

Identity types (x = y in A) encode mathematical equality between terms x and y of the same type A. The formation rule allows asking whether two terms are equal. The introduction rule provides reflexivity: for any term x, there is a term refl_x of type x = x. The elimination rule, known as path induction, allows reducing statements about equality to the reflexive case.

Equality constraints allow type-level programming by asserting that two types are equal. This enables writing instances that only apply when certain type equalities hold, effectively performing computations at the type level. For example, an instance can be written that says 'for any type a, lists of a are Foldable with length returning Int'. This allows overriding default behavior in specific cases while maintaining compatibility with existing code that already compiles with the general type signatures.

The equality of types judgment is written as A = B type, presupposing that both A and B are types. The meaning explanation requires four things: (1) For any X, X in B assuming X in A; (2) X in A assuming X in B; (3) For any X,Y, X=Y in B assuming X=Y in A; (4) X=Y in A assuming X=Y in B. This ensures types contain the same elements with the same equivalence relation.
Prerequisite Knowledge
- Concept 01Basic familiarity with OCaml syntax, particularly how variables (names) and literals (constants) are declared and used.
- Concept 02The conceptual definition of static typing and the fundamental goal of type inference algorithms.
- Concept 03The concept of a Type Environment (often denoted as Gamma) and how it maps identifiers to their respective types or type schemes.
- Concept 04An introductory understanding of type constraints and how equality between types is formulated as equations.
Subsequent Learning
- Step 01Implementing constraint generation for function applications and lambda abstractions.
- Step 02The unification algorithm (Robinson's Unification) to solve the generated type constraints.
- Step 03Let-polymorphism and the generalization of types using type variables and quantifiers (Algorithm W).
- Step 04Understanding the value restriction in OCaml and how type inference handles mutable state and side effects.
Type Basics
0:00- 1
Constants infer fixed types without constraints.
- 2
Environment lookup determines variable types.
- 3
Missing bindings cause inference failure.
Bidirectional Type Inference
While Hindley-Milner (HM) type inference is highly regarded for reconstructing types globally without requiring annotations, it often produces notoriously confusing error messages and struggles to scale when advanced type system features are introduced. Bidirectional Type Inference serves as a prominent alternative. Instead of generating and solving a global network of constraints from names and constants, bidirectional typing splits the process into two distinct modes: synthesizing (inferring) types and checking terms against expected types. This localized, directional flow of type information yields highly predictable and easily localizable error messages, and it gracefully accommodates modern language features—such as subtyping, GADTs, and higher-rank polymorphism—that render pure HM inference undecidable or overly complex.
Implementing constraint generation for function applications and lambda abstractions.

When processing a function application node, three types are involved: the overall expression type, the function type, and the argument type. The constraint generated is that the function type must be an arrow type (t3 → t4), where t3 is the argument type and t4 is the result type. This reflects the fundamental property that applying a function to an argument produces a result whose type depends on both the function's output type and the argument's type.

The gen function implements the visitor pattern for traversing lambda calculus expressions. For variables, it generates equivalences between the variable's type in the environment and the input annotation. For abstractions, it adds lambda argument types to the environment before processing the body. For applications, it recursively invokes gen on both sides and combines results with conjunctions. Constants generate immediate equivalences with their known types. This systematic traversal produces a complete constraint system that relates all type variables in the program.

Constraint generation involves recursively analyzing expressions while assigning fresh type variables and collecting equality constraints. For lambda functions, variables get fresh type variables, and function bodies generate constraints based on operators' expected types. For conditionals, the condition must be bool, and branches must return compatible types. For function applications, the function type must be C→D and the argument must be C. These constraints are collected mechanically without knowing final types, then solved to determine the most general type for the entire expression.

Each AST node generates specific constraints: function nodes require function types from parameter to body; function applications enforce function-argument compatibility; if expressions mandate boolean conditions and equal branch types; let bindings ensure value-body type consistency. The process recursively collects constraints from child nodes and adds node-specific constraints, building a complete constraint set for unification.

When processing a function application, a new type variable is introduced for the result type. Two constraints are generated: (1) The type of the function being applied must equal the type of the left-hand sub-expression (which should be a function type); (2) The type of the argument must equal the domain type of that function. This creates equations between type expressions that will be solved during unification.
The unification algorithm (Robinson's Unification) to solve the generated type constraints.

Unification is an algorithm for solving equations between terms by finding substitutions that make them equal. In type inference, unification collects local type constraints and combines them into a global solution. When unifying two function types, we recursively unify their left and right components. If unification fails (such as when a type variable occurs within itself), we encounter an 'occurs check' failure, indicating an infinite type.

Unification is the core algorithm used in type inference to solve type constraints. When encountering an application like `reverse X`, the type checker introduces a meta-type variable (alpha) for X and another (beta) for the instantiation of reverse. The constraint alpha ~ list beta is generated, meaning these two types must be equal. Solving this constraint produces a substitution that replaces meta-variables with concrete types. This process, developed by Alan Robinson, transforms the problem of finding a consistent type assignment into a logical puzzle of satisfying equality constraints between types.

Robinson's unification algorithm processes equations one at a time. For equations with function symbols (arrow types), the only way to unify is to unify corresponding subterms. The algorithm handles variables by substituting them with non-variable terms, removes trivial equations, and recursively unifies arguments for function symbols. It terminates because it reduces the number of variables, function symbols, or equations at each step. The algorithm correctly finds the most general unifier when one exists.

The unification algorithm solves systems of type constraints by iteratively applying substitutions to make both sides of each constraint identical, similar to solving algebraic equations through variable elimination; the solution is a sequence of substitutions that transforms each constraint into a tautology (e.g., int = int), and different processing orders may yield different valid substitution sequences.

Unification is the core algorithm for solving type equations. It recursively compares type structures: when a type parameter first matches a concrete type, it gets assigned that value; when two type parameters match each other, they are unified to represent the same type. The algorithm handles failures when types cannot be made identical. This process works for identity relations but requires modifications for assignability and constraints. After unification, instantiation checks whether inferred types actually satisfy constraints and whether function parameters are assignable to their declared types.
Let-polymorphism and the generalization of types using type variables and quantifiers (Algorithm W).
![[ML24] Rethinking the Value Restriction](https://i.ytimg.com/vi_webp/C1g_PO_xcI8/maxresdefault.webp)
The ML type system distinguishes monotypes (τ, no polymorphism) from type schemes (Σ, with polymorphic quantifiers). A fundamental restriction prohibits polymorphic values on function left sides. Two core rules enable polymorphism: generalization (quantifying unused type variables) and instantiation (substituting specific types). These rules alone don't form an algorithm. Two simplifying principles make ML implementable: prenex polymorphism (quantifiers only at top level) and let polymorphism (generalize only at let bindings, instantiate at uses). Together, they define Algorithm W, making type inference practical while preserving expressivity.

Algorithm W is a type reconstruction algorithm for implicit polymorphism. It takes typing context gamma and expression e, returning substitution s and monotype a. The language has monotypes (function types, type variables) and polytypes (for all alpha, monotype). Expressions include variables, lambda abstraction, application, and let binding. The typing context extends with type bindings but lacks explicit type variable declarations. The generalization rule converts monotypes to polytypes when type variables are not free in the context. The specialization rule instantiates polytypes to concrete types. For variables, we check for type bindings and apply specialization by introducing fresh type variables. For lambda abstraction, we introduce a fresh type variable and recursively type the body. For application, we recursively type both expressions, then unify the function type with the argument type using the unification algorithm. The unification algorithm solves type equations by recursively handling variable assignments and function type matching.

Three essential helper functions support Algorithm W: substitution composition applies substitutions sequentially, instance generates fresh type variables for type scheme instantiation, and generalization identifies free type variables not bound in the environment to promote polymorphism. The MGU algorithm performs type unification recursively: variables unify by substitution, basics must match, and function types require unifying inputs then outputs. The result is composed substitutions. This systematic approach enables automated type inference while preserving type safety and polymorphism.

Parametric polymorphism enables code to operate uniformly across all data types through type variables that range over types. An expression with polymorphic type α → α can have many concrete instantiations like int → int or string → string. However, let polymorphism imposes a crucial restriction: functions are only generalized as polymorphic after their binding site. During definition, using a function with a specific type (like id 5) constrains it to that type rather than allowing polymorphism. This explains why defining fun id x = id 5 results in int → int instead of α → α—the use during definition prevents generalization.

Let polymorphism maintains the two-layer type system of monotypes and polytypes from predicative polymorphism while introducing let binding as a new construct. The typing context now has two variable sources: lambda abstractions (monotypes) and let bindings (polymorphic types). The typing rule for let binding requires verifying the bound expression matches the specified polymorphic type, then augmenting the context. This explicit typing eliminates the need for complex reconstruction algorithms while enabling polymorphic variable introduction.
Understanding the value restriction in OCaml and how type inference handles mutable state and side effects.

The value restriction is a type system rule that prevents polymorphic mutable values from changing type, ensuring type safety by guaranteeing that once a polymorphic mutable value is instantiated with a specific type, it can never be changed to hold a different type; this rule is implemented in OCaml using weak type variables and similarly appears in Java through array store exceptions, which prevent storing incompatible types in mutable polymorphic collections.

Pure programming avoids side effects and state changes. In OCaml, variables are immutable - defining a new variable with the same name shadows rather than changes the original. To enable mutable data, OCaml uses references, which are memory blocks that can be changed. References have types based on stored values (int ref, string ref). The ref constructor creates references, the exclamation point dereferences to access values, and the colon-equal operator updates values. Memory can be visualized as boxes pointing to locations storing values. Two variables can reference different memory blocks storing the same value (structural equality) or the same memory block (physical equality). OCaml provides operators for both: = checks structural equality, == checks physical equality. The semicolon operator evaluates two expressions, ignoring the first and returning the second - useful for side effects where you care about the update but not the return value. This enables implementing counters and other stateful operations where the side effect matters more than the computed value.

OCaml is a functional programming language where variables are immutable by default (using 'let' binding), and mutable state requires explicit 'ref' types; the language uses 'let name = value in expression' syntax for scoping, semicolons for sequencing expressions with side effects, and 'Array.init' for creating arrays with fixed sizes. Pure functions (no side effects) are a cornerstone of functional programming, enabling predictable behavior and easier reasoning about code.

This extensive section covers pure functions, state management, and OCaml language fundamentals. A pure function has two key properties: determinism (same inputs produce same outputs) and no side effects (no modification of external state). Pure functions are predictable, testable, and reusable. Functions with side effects are problematic because they make code harder to reason about and maintain. The section addresses state management challenges in concurrent programming. As computers moved to multi-core processors, shared mutable state caused race conditions and concurrency bugs. The critical section problem occurs when multiple threads access shared resources simultaneously. Functional programming solves this by using immutable state and avoiding shared mutable state. Instead of modifying state in place, functional programs create new state based on the current state. This approach eliminates race conditions and makes concurrent programming safer. The section then introduces OCaml, a functional programming language with strong type inference. The compiler automatically determines types without explicit declarations. Expressions evaluate to values, and all values have types (int, float, string, bool). Lists are homogeneous sequences of elements of the same type. The empty list is represented as []. The cons operator (::) adds elements to the front of lists. Pattern matching allows matching values against patterns and executing different code for each match. The 'if' expression returns a value rather than executing a block. The 'let' expression binds names to values. Functions are defined using 'let function_name parameters = body'. Functions are first-class citizens, meaning they can be passed as arguments and returned as values. Higher-order functions like List.map apply a function to each element of a list. List.fold_left reduces a list to a single value by applying a function to an accumulator and each element. List.filter keeps only elements that satisfy a predicate function. The type system ensures type safety at compile time. The option type ('a option = None | Some of 'a') represents values that may or may not be present. The result type ('a result = Ok of 'a | Error of 'a') represents computations that may succeed or fail. Pattern matching on these types allows handling different cases safely. This type safety prevents runtime errors and makes code more robust.
![[ML24] Rethinking the Value Restriction](https://i.ytimg.com/vi_webp/C1g_PO_xcI8/maxresdefault.webp)
The ML type system distinguishes monotypes (τ, no polymorphism) from type schemes (Σ, with polymorphic quantifiers), prohibiting polymorphic values on function left sides. Two core rules enable polymorphism: generalization (quantifying unused type variables) and instantiation (substituting specific types). Two simplifying principles make ML implementable: prenex polymorphism (quantifiers only at top level) and let polymorphism (generalize only at let bindings, instantiate at uses). These principles define Algorithm W. However, adding mutable references breaks soundness because different uses of the same value can share type-dependent resources invisible to the context. The value restriction solves this by limiting generalization to syntactic values (expressions that don't allocate references or create new types), though this introduces complexity requiring programmers to understand syntactic value definitions.
Type Basics
0:00- 1
Constants infer fixed types without constraints.
- 2
Environment lookup determines variable types.
- 3
Missing bindings cause inference failure.
Bidirectional Type Inference
While Hindley-Milner (HM) type inference is highly regarded for reconstructing types globally without requiring annotations, it often produces notoriously confusing error messages and struggles to scale when advanced type system features are introduced. Bidirectional Type Inference serves as a prominent alternative. Instead of generating and solving a global network of constraints from names and constants, bidirectional typing splits the process into two distinct modes: synthesizing (inferring) types and checking terms against expected types. This localized, directional flow of type information yields highly predictable and easily localizable error messages, and it gracefully accommodates modern language features—such as subtyping, GADTs, and higher-rank polymorphism—that render pure HM inference undecidable or overly complex.
let's start defining the type inference relation and we'll start with the two simplest cases which are constants an integer constant i always has type int and generates no constraints likewise a boolean constant b is always of type bool and generates no constraints let's write down a couple examples in the empty environment the integer constant 3110 has type int and it generates no constraints the empty set so i'm using empty curly braces here to represent both the empty environment and the empty set of constraints hopefully that makes sense both of them are sets it's just the environment is a set of bindings and constraints are a set of type equations likewise in the empty environment the integer constant true has type bool and generates no constraints that must hold on times type inference for names is almost as easy in a static environment end n has whatever type the environment says it has and that generates no constraints now of course if that name was not present in the environment that is there was no binding for that name then type checking or type inference rather would fail at this point because there is no value corresponding to n of n suppose we have the static environment that binds x to type int in that environment we go to infer the type of x that produces int as the type along with no constraints now suppose we were in the empty environment and we tried to type check x x is not bound in that environment so this would fail there is no type that can be inferred here perhaps we could write a slash through the turn style if we were working on pencil and paper uh here i'll just put the slash in the middle to say we can't infer finally suppose we wanted to type check the plus operator how do we infer the type for it well every program that we write in this language ought to start off in an initial static environment that has the types for the three boolean operators so that environment should always have in it that plus is of type int arrow int arrow int and that should also have the same type for multiplication and a similar type for less than or equal to given that the name rule tells us how to infer the type of plus just look it up in the environment so that must have type int arrow int arrow int and generate no constraints here are the three bindings then that should be in every initial static environment when we start off doing type inference for this language plus and times and less than or equal to should always be bound to these types
Up Next

Camera Calibration with OpenCV Python | Computer Vision Tutorial
@NicolaiAI
110.2K views•2021-03-28

BitTorrent Protocol Explained: Piece Selection & Peer Choking
@StevenGordonAU
481 views•2013-02-22

Let Polymorphism: Type Schemes in HM Inference (OCaml) | Chapter 9
@MichaelRyanClarkson
3.4K views•2021-08-08

Enigma Machine Mechanics: WWII Encryption Explained
@JaredOwen
13.2M views•2021-12-11
Related Study Plans & Knowledge Roadmaps
Structured learning paths in Computer Science