Code Sync
Set-theoretic types allow Elixir to enforce static compile-time safety and exhaustiveness across legacy dynamic code without altering the underlying runtime.
AI-assisted summary of Bringing Types to Elixir by Giuseppe Castagna and Guillaume Duboc | ElixirConf EU 2023 — original source. Check the original for full context.
Source published:
Key ideas
- A set-theoretic type system for Elixir has been developed through 1.5 years of PhD research under Giuseppe Castagna and José Valim, designed to provide ahead-of-time static analysis natively within the Elixir compiler. (0:25)
- The framework models types literally as sets of values, utilizing unions, intersections, singletons, and negation types to capture multi-clause function contracts and pattern matching without false positives. (2:20)
- Gradual typing is addressed using a novel 'strong function' classification that guarantees type soundness at dynamic boundaries by verifying that inputs outside a domain either fail at runtime or evaluate within the declared codomain. (19:17)
- Developer adoption will prioritize typing patterns, guards, and struct definitions first, while legacy Erlang typespecs will continue to coexist alongside the new compiler-integrated type system. (28:47)
Detailed notes
## Background This presentation was published on YouTube by Code Sync on October 23, 2023, featuring a talk delivered at ElixirConf EU 2023. According to the page description, speaker Guillaume Duboc discusses the design of a set-theoretic type system tailored for Elixir, exploring how it catches common bugs, improves code quality feedback, and uniquely fits the language's semantics.
## TL;DR Guillaume Duboc presents the culmination of eighteen months of PhD research supervised by Giuseppe Castagna and José Valim to introduce set-theoretic static typing to Elixir. The presentation outlines core mechanisms such as function intersections, singleton types, complement/negation types, parametric polymorphism, and extended protocol composition. It demonstrates how pattern matching and map access rules provide rich typing context for ahead-of-time analysis, while introducing novel 'strong function types' and dynamic intersections to enable sound gradual typing. During the Q&A, Valim and Castagna join Duboc to unpack the mathematical necessity of negation for subtyping, early adoption paths via struct annotations, and the immense theoretical effort required to formalize Elixir map subtyping.
## Atmosphere & style The presentation carries an atmosphere of intellectual excitement and collaborative academic rigor balanced by practical software engineering realism. Guillaume Duboc delivers a clean, focused, and pedagogical walkthrough of complex type theory, while the dynamic shifts into a relaxed, good-humored camaraderie during the Q&A as José Valim and Giuseppe Castagna jump in to add color and field tough theoretical queries. Watching the session feels like witnessing a pivotal milestone in Elixir's evolution, where deep formal mathematics meets real-world developer ergonomics amidst enthusiastic audience engagement and shared relief over surviving marathon design meetings on map types.
## Bulletin - [00:25](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=25s) A set-theoretic type system for Elixir has been developed through 1.5 years of PhD research under Giuseppe Castagna and José Valim, designed to provide ahead-of-time static analysis natively within the Elixir compiler. - [02:20](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=140s) The framework models types literally as sets of values, utilizing unions, intersections, singletons, and negation types to capture multi-clause function contracts and pattern matching without false positives. - [19:17](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1157s) Gradual typing is addressed using a novel 'strong function' classification that guarantees type soundness at dynamic boundaries by verifying that inputs outside a domain either fail at runtime or evaluate within the declared codomain. - [28:47](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1727s) Developer adoption will prioritize typing patterns, guards, and struct definitions first, while legacy Erlang typespecs will continue to coexist alongside the new compiler-integrated type system.
## Key takeaways - [00:25](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=25s) Existing Elixir typespecs and Dialyzer act primarily as external documentation tools, whereas the new system introduces native compile-time static type checking. - [02:20](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=140s) Naive union types destroy return precision across multi-clause functions, requiring function intersection types (`and`) to track specific input-to-output relationships. - [05:19](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=319s) Negation types allow concise description of complement sets, solving the impractical requirement of enumerating unbounded sets of atoms or terms in truthy-coalescing operations. - [25:50](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1550s) Under set-theoretic subtyping, testing if S is a subtype of T reduces algorithmically to checking whether the intersection of S and the negation of T is empty (S ∩ ¬T = ∅). - [09:09](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=549s) The type system upgrades Elixir protocols by turning protocol types into exact unions of conforming types, allowing element parameterization and compositional intersections like Traversable. - [12:56](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=776s) Pattern matching in function clauses and case statements supplies deep structural information, allowing the compiler to detect non-exhaustive matches and dead code clauses. - [16:09](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=969s) With return type deduction (`result -> _`), the compiler statically infers output types across all pattern-matched branches without requiring explicit return annotations. - [16:09](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=969s) Map access syntax is formally partitioned: dot notation requires guaranteed static field presence, whereas bracket notation returns a union with nil for potentially absent keys. - [19:17](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1157s) Unannotated dynamic code returns an intersection of the inferred bounds and `dynamic` (`(integer or boolean) and dynamic`), permitting flexible usage without generating false positives. - [28:47](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1727s) Structs are represented as maps with a fixed `__struct__` module tag atom, enabling them to fit cleanly into existing map typing semantics. - [28:47](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1727s) The initial user-facing rollout will focus on typing patterns, guards, and struct definitions before requiring manual annotations on general functions. - [38:32](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=2312s) Formalizing open map typing with key exclusions was the most mathematically complex aspect of the project, requiring extensions to existing academic type theory.
## Detailed walkthrough ### [00:25](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=25s) Project Genesis and the Motivation for Static Typing in Elixir
Guillaume Duboc opens the presentation by introducing the fruits of his PhD research, conducted under the joint supervision of type systems expert Giuseppe Castagna and Elixir’s creator, José Valim. The initiative began two years prior through email exchanges between Castagna and Valim, who realized that bringing formal type theory to Elixir required dedicated academic horsepower. As Duboc recounts, “This project was initiated two years ago during mail exchanges between José and Giuseppe when they decided to bring on a PhD candidate to help realize their ideas, and now after one year and a half of work I'm super excited to present you the type system that we've designed for Elixir.” The primary objective of the talk is to demonstrate the system's capabilities and show how it materially elevates the Elixir developer experience.
Addressing the obvious question of why Elixir needs types, Duboc acknowledges that Elixir already features dynamic runtime type checks. Developers can write guard clauses and runtime tests to inspect inputs, but because these checks occur exclusively while code is executing, defects are often uncovered far too late. Similarly, while Erlang-style typespecs exist, Elixir utilizes them primarily for documentation purposes. Although third-party tools such as Dialyzer can consume typespecs for external static analysis, the Elixir compiler itself has historically remained hands-off. Duboc explains the fundamental shift this project introduces: “And this is precisely what we want to do: we want to introduce a system of types that will be used before running your programs, before compilation, to analyze the behavior of your programs and detect bugs.” He illustrates this with a straightforward bug: an invalid expression where a boolean negation operator is erroneously applied to an integer, which should immediately flag a compile-time type error blaming the addition operator.
- Project timeline: Conceived 2 years prior via email correspondence between José Valim and Giuseppe Castagna; developed through 1.5 years of PhD work by Guillaume Duboc. - Existing status quo: Dynamic type checks occur strictly at runtime; typespecs exist chiefly as documentation with minimal native compiler enforcement. - New paradigm: Ahead-of-time static type analysis executed prior to code compilation to catch bugs before execution.
---
[02:20](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=140s) Function Contracts, Union Types, and the Need for Intersections
Duboc moves into how types serve as formal contracts governing the inputs and outputs of functions. He proposes an explicit type annotation syntax marked by a dollar sign (`$`). Starting with a simple toy function, `negate/1`, which manipulates an integer, an annotation `$ (integer()) -> integer()` guarantees that passing an integer yields an integer. When invoked inside another annotated function like `subtract/2`—typed as `$ (integer(), integer()) -> integer()`—the code typechecks smoothly because `negate(b)` is guaranteed to evaluate to an integer.
Complications emerge, however, when `negate/1` is extended with multiple clauses to support additional data types such as booleans. If a developer attempts to update the contract using a naive union type: ```elixir $ (integer() or boolean()) -> (integer() or boolean()) def negate(x)... ``` Duboc notes: “And this is a union type, and it has to be understood in the most intuitive meaning that if a value has type T1 or T2, then it has either type T1 or type T2.” While mathematically straightforward, this union signature destroys precision. When `negate(b)` is called inside `subtract/2`, the typechecker only knows that the return value is `integer() or boolean()`. Because Elixir's numeric `+` operator cannot accept a boolean, `subtract/2` fails compilation with a type error—even though an integer argument passed to `negate/1` will always return an integer in practice.
To resolve this loss of precision, the type system introduces function type intersections using `and`: ```elixir $ ((integer()) -> integer()) and ((boolean()) -> boolean()) def negate(x)... ``` Duboc highlights the exact semantics: “Here this type annotation tells you that negate has both the type of functions that when given an integer value they return an integer, and when given a Boolean they return a Boolean.” Armed with this intersection, the typechecker can verify that passing an `integer()` into `negate/1` specifically produces an `integer()`, allowing `subtract/2` to typecheck without false positives.
---
[05:19](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=319s) Singleton Types, Negation Types, and Expressiveness
Beyond basic unions and intersections, the type system includes set-theoretic primitives like singleton and negation types to precisely model idiomatic Elixir branching patterns. Duboc demonstrates this with a simple logical negation function, `logical_neg/1`. The first clause matches when the input is specifically the atom `false` or `nil`, returning `true`. This behavior is represented using singleton types—types that denote a single specific value: ```elixir $ (false or nil) -> true ``` To express the fallthrough clause—which accepts any value *except* `false` or `nil` and returns `false`—the system provides complement/negation types: ```elixir $ (not (false or nil)) -> false ``` Here, `not (false or nil)` encompasses the set of all Elixir terms excluding those two truthy-violating primitives.
Duboc then transitions to parametric polymorphism, where functions operate generically over arbitrary types using type variables. Demonstrating with `Enum.map/2`, the annotation defines list elements via type variable `a` and mapped outputs via `b`, quantified using a `where` clause: ```elixir $ (list(a), (a -> b)) -> list(b) where a: term, b: term ``` Upper bounds can be placed on type variables to constrain valid types, though here they default to `term`. Duboc emphasizes that the engine handles instantiation automatically: “The type system also performs local type inference, meaning that you do not have to specify what A and B are when you apply those functions.” If `map` is invoked over a list containing integers and booleans with a transformation function mapping numbers to integers, the compiler statically infers that the resulting value is `list(integer())`.
Furthermore, polymorphism and intersections compose naturally. For an Elixir short-circuiting logical OR helper taking two arguments: 1. If the first argument is `false or nil`, it returns the second argument unmodified, yielding type `(false or nil, a) -> a`. 2. If the first argument is truthy, it discards the second argument and returns the first, typed as `(b, term) -> b where b: not (false or nil)`. Intersecting both clauses forms an exact static specification of Elixir’s truthy-coalescing runtime behavior.
---
[09:09](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=549s) Enhancing Protocols with Static Types: Parameterization and Composition
Duboc demonstrates that having this sophisticated static foundation directly enhances native Elixir language constructs, notably protocols: “Having this precise system of static types means that we can improve the usability of some of Elixir's features, and one of those is to improve the use of protocols.” In Elixir, protocols define polymorphic interfaces across data structures, such as `String.Chars` with its required `to_string/1` implementation. A critical concept is the protocol type representing all values implementing that protocol. In standard Elixir, the compiler automatically generates `String.Chars.t()`, but historically approximates it as `term()`. Under the set-theoretic engine, `String.Chars.t()` becomes an exact union of every type implementing the protocol—subsuming primitive types like integers and strings as well as user-defined structs.
This breakthrough unlocks two longstanding requests for protocols: 1. **Protocol Parameterization:** Specifying the inner types handled by a protocol container. For example, `Enum.into/2` takes an `Enumerable` and a `Collectable`, pushing items from one into the other. Its existing typespec lacks the capacity to describe element types. With the new system, `Enumerable.t(a)` designates an enumerable yielding elements of type `a`. Consequently, `into/2` is cleanly typed as: ```elixir $ (Enumerable.t(a), Collectable.t(b)) -> Collectable.t(a or b) ``` 2. **Protocol Composition:** Combining requirements through intersection types. Developers can declare an alias for data structures implementing multiple protocols simultaneously, such as a `Traversable.t(a)`: ```elixir @type traversable(a):: Enumerable.t(a) and Collectable.t(a) ```
```mermaid flowchart TD accTitle: Protocol Composition via Intersection accDescr: Diagram showing how Enumerable and Collectable protocols intersect to define Traversable.
subgraph Protocols ["Protocol Types"] E["Enumerable.t(a)"] C["Collectable.t(a)"] end
subgraph Composition ["Intersection Type"] T["Traversable.t(a)<br/>(Enumerable.t(a) and Collectable.t(a))"] end
subgraph Implementation ["Concrete Conforming Implementations"] S["IO.stream / Streams<br/>(Element specialized to String.t)"] end
E --> T C --> T T --> S ```
Duboc illustrates this with a small `echo` function that requires an argument supporting both enumeration and collection. By passing an `IO.stream`—which in Elixir satisfies both `Enumerable` and `Collectable`—to `echo`, developers get a fully typechecked, bidirectional read-and-write loop that mirrors input directly back to output. Because the elements inside the traversable can be specifically constrained to strings, operations on those elements are safely guaranteed by the compiler.
[12:56](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=776s) Pattern Matching as a Rich Source of Type Information
Guillaume Duboc explains how set-theoretic intersections and parametric types can type check complex patterns, such as a read/write loop that capitalizes strings passed across protocols. A central challenge in designing a type system for Elixir is accounting for its ubiquitous pattern-matching mechanism, which appears in identical semantics across both top-level function clause definitions and `case` expressions. Pattern matching inherently contains a wealth of typing information that the type checker can extract directly to provide actionable compiler feedback and infer precise signatures.
To illustrate, Duboc presents a `result` type defined as a map containing an `:output` field. When `:output` is `:ok`, the map also defines a `:socket` field holding a value of type `socket`. Conversely, if `:output` is `:error`, the map defines a `:message` field holding either the atom `:timeout` or a 2-tuple containing the atom `:delay` and an integer.
```mermaid flowchart TD accTitle: Result Type Map Structure accDescr: Branching structure of the result map type showing output ok with socket versus output error with message variants.
subgraph ResultMap["result type"] FieldOutput["output::ok |:error"] end
subgraph OkBranch["When output is:ok"] SocketField["socket: socket"] end
subgraph ErrorBranch["When output is:error"] MessageField["message::timeout | {:delay, integer}"] end
ResultMap -->|":ok"| OkBranch ResultMap -->|":error"| ErrorBranch ```
When a developer writes a handler function matching on this structure, missing cases immediately produce static warnings:
- If a function clause only pattern matches on `output::ok` and `message::timeout`, it misses the error branch containing the `{:delay, integer}` tuple. Duboc emphasizes: “the type Checker would give you here A type warning sending saying that this pattern matching is uh non-exhaustive and it would output you it would compute for you the precise type of the values that escape your pattern matching in all cases.” - If an author covers the valid branches and then appends an extra clause attempting to handle a socket when one is present, the type system detects that the prior clause already exhausted that condition. The compiler flags this as an unused branch, highlighting the exact clause that will never be reached at runtime.
[16:09](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=969s) Return Type Inference and Map Access Typing
Because pattern matching yields such high-fidelity data, the type system can infer exact return types without requiring manual annotations for every clause. Duboc highlights the syntax `result -> _`, which specifies only the function's input type while directing the type checker to calculate the output type automatically:
```mermaid flowchart LR accTitle: Pattern Matching Return Type Deduction accDescr: How matching clauses map to inferred return types, culminating in a union or intersection type.
subgraph InputClauses["Clauses"] C1["Clause 1: output is:ok"] C2["Clause 2: message is:timeout"] C3["Clause 3: rest of values"] end
subgraph InferredTypes["Clause Return Types"] R1["socket"] R2[":timeout"] R3["integer"] end
C1 --> R1 C2 --> R2 C3 --> R3
R1 --> FinalReturn["socket |:timeout | integer"] R2 --> FinalReturn R3 --> FinalReturn ```
- When the first clause matches `output::ok`, the returned socket is typed as `socket`. - When the second clause matches `message` as an atom, the type checker narrows it to the literal atom `:timeout`. - When the third clause catches the remaining case, the message must hold the `{:delay, integer}` tuple, typing the extracted second element directly as `integer`. - Aggregating all branches yields a return type of `socket |:timeout | integer`, or an even more granular intersection type describing exact return types for every valid sub-domain of the specified input.
Duboc then addresses the syntax and typing rules devised for Elixir maps. Consider a map type `T` where field `foo` must hold an atom, optional field `bar` holds an atom if defined, and any remaining atom keys map to integers:
| Expression | Evaluated Type | Static Typing Rule / Warning | |:--- |:--- |:--- | | `m.foo` | `atom` | Permitted; key is guaranteed to exist. | | `m.bar` | *Type Error* | Dot notation (`m.key`) requires the key to be statically guaranteed to exist. | | `m[:bar]` | `atom \| nil` | Bracket notation allows undefined keys, returning `nil` when absent. | | `m[:other]` | `integer \| nil` | Any other atom key matches the dynamic map spec or evaluates to `nil`. | | `m[123]` | `nil` | Integer keys are not defined on `T`; triggers a type warning for accessing an impossible field. |
[19:17](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1157s) Gradual Typing, Strong Functions, and Dynamic Intersections
Because Elixir is an established language with an extensive ecosystem, the type system cannot demand a total rewrite. As Duboc notes, “adding types to Elixir means that we have to take into account that a large part of the code is already written in fact all of the code is already written.” To solve this, the team adopts gradual typing via a `dynamic` type that can materialize as any concrete type at runtime, offering a relaxed soundness property: if a program expression $e$ has type `integer`, any value it successfully evaluates to at runtime will be an integer.
However, standard gradual typing (as found in TypeScript) allows `dynamic` to silently bypass annotations without enforcing safety at boundaries. Duboc contrasts two approaches using identity functions:
```mermaid flowchart TD accTitle: Weak vs Strong Function Types Under Dynamic Inputs accDescr: Contrasting id_weak (which propagates dynamic) against id_strong (which enforces an integer return).
subgraph WeakID["id_weak(dynamic)"] WIn["Input: dynamic"] --> WOut["Output: dynamic"] WNote["A boolean passed as dynamic passes through as boolean"] end
subgraph StrongID["id_strong(dynamic)"] SIn["Input: dynamic"] --> SCheck{"Runtime Guard / Type Check"} SCheck -->|"Matches Domain"| SOut["Output: integer"] SCheck -->|"Outside Domain"| SFail["Fails with Type Error"] end ```
Duboc defines this mechanism: “a strong function type is a function type that outside of its domain it's going to fail on the type error or it's going to give you a value which is inside of its co-domain and here the co-domain is integer for id_strong that makes us able to give a strong guarantee on the types that strong functions output and we do that without modifying at all the Elixir compilation because we take into account what is already being performed by The Elixir runtime.”
The type checker also handles unannotated code dynamically without producing unnecessary false positives. For an unannotated `negate` function taking `dynamic` and returning an integer or a boolean: - Simply typing the return as `integer | boolean` would break calling code like `subtract(negate(x), 5)`, because passing `integer | boolean` to `+` or `-` triggers an error. - Instead, the type checker infers the intersection `(integer | boolean) & dynamic`. - This relaxed intersection type signals that the return value is bounded by `integer | boolean`, but may be treated as dynamically compatible in numeric contexts. - Expressions like `subtract` type check successfully (failing at runtime if a boolean emerges), while provably invalid operations—such as passing the result to the string concatenation operator `<>`—are rejected at compile time.
[23:22](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1402s) Research Status, Compiler Roadmap, and Q&A
Duboc concludes by reviewing the project's milestones and outlining next steps: - A complete formalization of the set-theoretic type system for Elixir has been established, accompanied by dedicated syntax specifications. - A functional research prototype was implemented to validate and type-check the foundational examples. - The academic paper formalizing the type system is available in draft form on Duboc's academic webpage. - During the second semester, the team plans to transition from research to production by integrating the type checker directly into the upstream Elixir compiler.
Duboc offers attendees access to an experimental prototype to try out the examples, but warns that it is strictly a research artifact rather than a finished end-user tool. As applause breaks out, an organizer reminds the audience: “please do not share these URLs.” The session transitions to audience questions, with Duboc joking that he might redirect tough queries to his PhD advisor, Giuseppe Castagna. The first question from the floor focuses on the decision-making behind adding negation types versus requiring explicit type definitions.
[25:50](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1550s) The Role of Negation and Set-Theoretic Foundations
```mermaid flowchart TD accTitle: Set-Theoretic Typing Operations accDescr: Visualizing union, intersection, and negation operations used to express types and compute subtyping via emptiness checks.
subgraph Types["Type Universe"] T1["Type S"] T2["Type T"] end
subgraph Operations["Set Operations"] Union["S ∪ T (Union)"] Intersect["S ∩ T (Intersection)"] Negate["¬T (Negation: Values not in T)"] end
subgraph Subtyping["Subtyping Algorithm"] EmptyCheck["S ∩ ¬T = ∅?"] Result["S is a subtype of T (S <: T)"] end
T1 --> Intersect T2 --> Negate Intersect --> EmptyCheck Negate --> Intersect EmptyCheck --> Result ```
Responding to an audience inquiry regarding allowed versus disallowed inputs and return values, Guillaume Duboc explains that negation types introduce powerful expressive capabilities into the language. A negation type allows one to select any given type and represent strictly the set of values that do not belong to it. This directly mirrors the foundational mathematical theory underpinning the project: set-theoretic types. Under this framework, types are treated literally as sets of runtime values. For instance, the integer type represents the set of all integers ($0, 1, 2, 3, \dots$). Consequently, standard set operations can be applied directly to types: computing the intersection of two types ($T_1 \cap T_2$), taking their union ($T_1 \cup T_2$), or using negation to denote every value except those belonging to a specific set.
José Valim steps in to illustrate the practical necessity of negation by showing a slide example involving boolean logic and guards. When handling an expression like `not false`, attempting to express the resulting type positively without negation would require explicitly enumerating every alternative value that could possibly inhabit the system. Because atoms in the BEAM ecosystem are unbounded, a developer or compiler would be forced to enumerate an infinite list of atoms, followed by integers, lists, maps, and every other distinct data structure. As José puts it: “the negation is just very convenient because describing infinity would be hard.”
Giuseppe Castagna adds the theoretical perspective behind why negation is indispensable to the underlying algorithms. Relying on De Morgan's laws, intersections can be encoded through unions and negations. Furthermore, subtyping—verifying whether a type $S$ is contained within a type $T$—relies on an emptiness test: $S$ is a subtype of $T$ if and only if $S \cap \neg T = \emptyset$. Because the type-checking algorithms operate by systematically testing the emptiness of these derived sets, negation is fundamentally required from a formal point of view.
[28:47](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1727s) Struct Typing and Erlang Dependencies
Turning to how the proposed type system handles Elixir structs, an attendee asks whether there are concrete plans to type check them. Guillaume confirms that structs are already handled cleanly: beneath their syntax, Elixir structs are maps containing a special `__struct__` field tied to the module name of the struct. As a result, the type checker evaluates them as map types where the struct tag field is fixed to that specific struct atom.
Another audience member asks whether the system will be able to type check Erlang dependencies and analyze how external libraries influence types across an Elixir codebase. Guillaume clarifies that for foreign codebases without native type annotations, developers would likely need to author interface signatures manually to dictate expected input and return types. While the set-theoretic approach could theoretically apply to Erlang due to its shared execution runtime, Guillaume has not focused on Erlang directly. Establishing a reliable static type system in Elixir, however, lays the groundwork for writing foreign function interfaces that external dependencies can safely pass through.
José adds a clarifying parenthesis regarding the implementation roadmap for structs: - The team's immediate focus centers entirely on typing patterns and guards, as existing guards already supply rich type information without demanding code changes from users. - In patterns, struct declarations match on names, but individual struct fields often have implicit type requirements (e.g., expecting a user name to be a binary) that are not currently tracked through the compiler. - José notes: “most likely the first contact that we are going to have as developers with the new type system is going to be through typing structs.” - Developers will probably type struct definitions and their fields long before annotating functions directly.
[32:08](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1928s) Gradual Migration, Dialyzer Type Specs, and Tooling
Addressing how developers will migrate from existing `@spec` annotations, an attendee notes that tools like ElixirLS extract type information from `@type` and `@spec` tags, and asks what the gradual adoption strategy will be. Guillaume jokingly redirects the question: “I think this is the question for José.” After a round of laughter from the room, José acknowledges the handoff with “Nice one. Nicely dodged.”
José explains that legacy type specs and the new set-theoretic annotations are designed to coexist for a prolonged period, and potentially forever, because type specs remain essential for Erlang interoperability. However, the new system intends to provide far deeper integration directly inside the Elixir compiler and developer tooling. Highlighting work introduced in his keynote regarding code fragments and IDE tooling, José emphasizes that language server maintainers will eventually consume type diagnostics, completions, and inferences directly from core Elixir rather than relying on external heuristic tools. This will establish tighter feedback loops in both code editors and the interactive IEx shell.
[33:54](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=2034s) Strong vs. Weak Function Types and Struct Invariants
A participant asks for clarification regarding the distinction between "weak" and "strong" function types, specifically querying how this mechanism interacts with structs when a function matches only on a struct module name rather than asserting guards across every individual struct field.
Guillaume initially explains that matching a struct pattern does produce a strong constraint because Elixir guarantees a structural check verifying that the incoming value is a map with the correct struct name. José then expands on the formal rule governing strong function types:
```mermaid flowchart LR accTitle: Strong Function Typing Rule accDescr: Evaluation flow showing how strong function types are verified by negating the domain and testing the function behavior.
Arg["Input Negates Domain (arg ∈ ¬Domain)"] --> Check{"Function Execution"} Check -->|"Fails / Raises"| Strong["Strong Function Type Verified"] Check -->|"Returns Codomain"| Strong Check -->|"Returns other dynamic value"| Weak["Weak Function Type"] ```
When evaluating a function signature $Domain \to Codomain$, the system examines what happens when an input value belongs to the negation of the domain ($\neg Domain$). If invoking the function with a value outside its declared domain either immediately raises an error or still returns a value within the declared $Codomain$, the signature is classified as strong. For instance, if an integer-expecting function raises when handed a boolean, it satisfies the strong condition. For structs, the function's strength depends on the operations performed inside: if an unvalidated field access subsequently triggers an exception when the type is violated, the function upholds the contract. José emphasizes that while this formal classification represents a major theoretical achievement for gradual typing, users should not feel burdened by it: “the concept of being strong it's like it's very important and we are very proud because it's like new theory for gradually typed languages but hope is that as developers we won't have to worry about it it's just going to be there.”
The session transitions to a question comparing Elixir's approach to TypeScript, where developers can construct union types derived from the keys or atoms of map structures. Guillaume begins answering by emphasizing that bringing dynamic key unions and precise map types into set-theoretic systems required significant extensions to existing academic type theory.
[38:32](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=2312s) Typing Maps and the Challenges of Map Subtyping
An audience member follows up with a question regarding map types—specifically whether the proposed type system supports specifying the type of any atom except for designated keys like `:board` and `:field`. The speaker confirms that this refers to a very specific extension to the typing of Elixir maps, validating that the system indeed handles it: “The feature he's talking about is being able to specify the type of any other atom except board and field; this is a very specific extension to typing of maps, and yeah, it works.” For those interested in the formal theory behind this capability, the speaker mentions that the full technical details will appear in an upcoming research paper.
The discussion highlights just how much effort went into tackling maps compared to other Elixir language constructs. José Valim steps in to confirm the difficulty, asking, “Maps were actually the hardest part of the work so far, right? Do you agree?” Guillaume Duboc agrees, and José elaborates on the behind-the-scenes struggles of the design process, emphasizing that while the resulting rules might look deceptively clean, they were the product of intense debate: “We have that, but it was lots of lots of work; it doesn't look like that, but sometimes we'd have three, four hours meeting just talking about maps and how to type them.” With that final insight into the development process, the talk concludes to enthusiastic applause from the audience.
- **Open map key exclusions**: The type system can express map types with open atom keys while explicitly excluding specific known keys (such as excluding `:board` and `:field`). - **Forthcoming theoretical paper**: The formal semantics and rules for these map extensions are slated for publication in a future academic paper. - **Design complexity**: Formalizing map types in a set-theoretic framework required marathon three- to four-hour design meetings, proving to be the most demanding aspect of the typing work to date.
## Notable quotes > [19:17](https://www.youtube.com/watch?v=gJJH7a2J9O8&t=1157s) adding types to Elixir means that we have to take into account that a large part of the code is already written in fact all of the code is already written. — Guillaume Duboc
## Mentioned Giuseppe Castagna, Guillaume Duboc, José Valim, Elixir, Erlang, Dialyzer, TypeScript, ElixirLS, IEx, BEAM, ElixirConf EU
## Video description SAVE THE DATE FOR NEXT EDITION: 18-19 April 2024 📆 🔥 Check https://elixirconf.eu for details about ElixirConf EU 2024! 🔥 Call for Talks is open and you can also join the waiting list for access to limited Very Early Bird pricing - don't miss the cheapest tickets!
About the talk: Set-theoretic types, and how they are uniquely suited to represent Elixir programs: Guillaume Duboc discusses the potential benefits of a type system designed for Elixir, such as detecting common bugs and providing direct feedback on code quality.
Follow us on social: Twitter: https://twitter.com/ElixirConfEU and https://twitter.com/CodeSyncGlobal LinkedIn: https://www.linkedin.com/company/27159258 Facebook: https://www.facebook.com/CodeSyncGlobal
Looking for a unique learning experience? Attend the next Code Sync conference near you! See what's coming up at: https://codesync.global
[Source](https://www.youtube.com/watch?v=gJJH7a2J9O8)