惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
Help Net Security
Help Net Security
腾讯CDC
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tailwind CSS Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
N
News | PayPal Newsroom
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
SegmentFault 最新的问题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Proofpoint News Feed
A
Arctic Wolf
B
Blog RSS Feed
Forbes - Security
Forbes - Security
P
Privacy & Cybersecurity Law Blog
Attack and Defense Labs
Attack and Defense Labs
V2EX - 技术
V2EX - 技术
P
Proofpoint News Feed
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
阮一峰的网络日志
阮一峰的网络日志
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
MyScale Blog
MyScale Blog
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学
W
WeLiveSecurity
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
罗磊的独立博客
The Last Watchdog
The Last Watchdog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Vulnerabilities – Threatpost
美团技术团队
Microsoft Security Blog
Microsoft Security Blog

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
Record type inference for dummies
2026-06-23 · via Hacker News

The reason I'm writing this post is because I actually wanted to write a more advanced post on type inference for anonymous records, but then I realized that most of my readers wouldn't understand the latter post in isolation. So I figured I would write this introductory post to teach people new to type theory the basics.

The reason I'm writing both posts is because I believe that good type inference for anonymous records is one of the big things holding back statically typed languages1 and not enough people appreciate this or understand why. I also think that there is a large disconnect between what programming language experts understand is possible and what lay programmers are comfortable or familiar with. To make a play on XKCD #2501:

So this post (and the next one) are going to be an exposition of where the field of type theory was at over three decades ago, which our industry still hasn't really caught up to, yet.

Anonymous records

I mentioned that this is a post about type inference for anonymous records and I've met some programmers who either don't know what anonymous records are or at least don't recognize them by that name, so I'll briefly touch upon them.

An anonymous record is a record that doesn't require an associated datatype declaration, and they're quite common in dynamically typed languages. For example, JavaScript calls them "objects":

{ name: "Alice", age: 25 }

Python calls them "dictionaries":

{ "name": "Alice", "age": 25 }

Ruby calls them "hashes":

{ :name => "Alice", :age => 25 }

# … or equivalently:
{ name: "Alice", age: 25 }

…, and Nix calls them "attribute sets":

{ name = "Alice"; age = 25; }

If you've ever worked with JSON then you've worked with anonymous records because JSON objects (just like JavaScript objects) are anonymous records.

Static typing

A smaller number of statically typed programming languages support anonymous records because statically typed languages usually prefer named datatypes.

For example, Haskell does not support anonymous records and requires a datatype declaration for all records, like this:

data Person = Person{ name :: Text, age :: Integer }

example :: Person
example = Person{ name = "Alice", age = 25 }

… but there are still some statically typed programming languages that do support anonymous records, like TypeScript:

{ name: "Alice", age: 25 } : { name: string, age: number }

… or C# (which calls them "anonymous types"):

new { Name = "Alice"; Age = 25 }

… or PureScript (which calls them records):

{ name: "Alice", age: 25 } :: { name :: String, age :: Int }

If all we needed was language support for record literals without any operations on records then it's fairly easy to infer their types. To do so, though, I'm going to introduce some type theory notation.

Let's start by defining a basic abstract syntax tree that says that an expression () can be either:

  • a (e.g. )
  • a (e.g. )
  • a record containing 0 or more fields (e.g. )

The way we would write that2 formally is:

… and the equivalent Haskell code would be something like:

type Identifier = Text

data Expression
    = Boolean Bool
    | String Text
    | Number Double
    | Record (Map Identifier Expression)

Fields can store arbitrary expressions, which means you can nest records, like:

We'll also need to define an abstract syntax tree for our inferred types, which can be either:

  • the type
  • the type
  • the type
  • a record type containing 0 or more fields (e.g. )

The notation we'd use for that is:

The equivalent Haskell code would be something like:

data Type
    = BooleanType
    | StringType
    | NumberType
    | RecordType (Map Identifier Type)

Now that we have a syntax for expressions and types we can define some type inference rules:

  • and always have type
  • a (like ) always has type
  • a (like ) always has type

We write out those rules using this notation:

If you've never seen this notation before, you can think of it as mathematical pseudocode for how to implement a type inference function. The equivalent Haskell function would be something like:

infer
    :: [(Identifier, Type)]
    -- ^ context, a.k.a. "Γ" (currrently unused)
    -> Expression
    -- ^ input expression
    -> Either Text Type
    -- ^ output inferred type (currently never fails)
infer context (Boolean _) = return BooleanType
infer context (String _) = return StringType
infer context (Number _) = return NumberType

Note: You can find the complete Haskell code in the Appendix.

Now suppose that we wanted to infer the type of a record literal like this one:

Normally we'd reason about the expression's type by hand like this:

  1. to infer the type of we need to infer the type of each field:

    A. first, infer the type of (which is )

    B. then, infer the type of (which is )

  2. now combine those into the final record type:

Type theorists have a notation for that sort of reasoning process, which looks like this:

That is known as a "typing derivation" and the way it works is that "outer" reasoning steps (like steps 1 and 2) go on the bottom and "inner" reasoning steps (like steps A and B) go on top.

If we take that reasoning process and generalize it to all records we might write something like this:

… which you can read as saying "if you want to infer the type of a record then infer the type of each field and replace each field with its inferred type". In Haskell this would be:

infer context (Record fields) = do
    fieldTypes <- traverse (infer context) fields
    
    return (RecordType fieldTypes)

… and we can verify this all works in the Haskell REPL:

ghci> :set -XOverloadedStrings -XOverloadedLists
ghci> infer [] (Record [("name", String "Alice"), ("age", Number 25)])
Right (RecordType (fromList [("age",NumberType),("name",StringType)]))

Field access

Any programming language worth its salt3 will also support record field access using something like dot notation (e.g. ).

For example, in Nix that would look like this:

nix-repl> { name = "Alice"; age = 25; }.name
"Alice"

So we'll add a type inference rule for field access, but first we need to extend our expression syntax to support dot notation:

… and now we can add this type inference rule:

This says that we can access a field named from an expression if 's inferred type is a record with a field named . The inferred type of the field becomes the inferred type of the field access.

The equivalent Haskell code would be something like:

data Expression =

    | FieldAccess Expression Identifier



infer context (FieldAccess expression field) = do
    expressionType <- infer context expression
    
    case expressionType of
        RecordType fieldTypes ->
            case Map.lookup field fieldTypes of
                Just fieldType ->
                    return fieldType
                Nothing ->
                    Left "missing field"
        _ ->
            Left "not a record"

This is the first type inference rule we've added that can fail. If we were to infer the type of an expression like then that wouldn't match that type inference rule (or any other rule) so we would reject the expression with a type error ("not a record").

However, the rule would also reject a field access if the field is missing, too. If we were to infer the type of that also doesn't match our type inference rule so we would reject the expression with a type error ("missing field").

Before we move on, let's test out this type inference rule on an example. Suppose that we want to infer the type of this expression:

Our reasoning process might go something like:

  • to know the type of the field access () I need the type of the record
    • to know the type of the record I need the type of the field
      • the type of the field (set to ) is
    • the type of the record is
  • the type of the field access () is

… and the equivalent formal derivation would be:

… and we can confirm in Haskell that the inferred type is indeed :

exampleRecord :: Expression
exampleRecord = Record [("name", String "Alice"), ("age", Number 25)]

exampleAccess :: Expression
exampleAccess = FieldAccess exampleRecord "age"
ghci> infer [] exampleAccess
Right NumberType

Variables

You might wonder why we don't just write the rule like this:

In other words, why don't we consult the expression's value instead of the expression's type when inferring the field's type? After all, that would make our reasoning process much more direct for that last example:

  • to know the type of the field access () I need the type of the field (set to )
    • the type of is
  • the type of the field access () is

… and the equivalent formal derivation would also be simpler:

Consulting the value instead of the type would work for our current (incredibly simple) programming language, but would no longer work once we add support for variables because then an expression like this would be rejected:

You can read that as assigning an expression () to a local variable () and then accessing the field from that variable. We can only infer a type for this expression if we consult 's type.

We can't consult 's value because right now is just an unevaluated variable (not a record literal). We could evaluate our expression first to get 's value but that would defeat the purpose of type checking: normally you type check an expression before you evaluate the expression in order to catch mistakes before evaluation begins4.

On that note, let's go ahead and add variables and variable assignment to our very minimal language:

Now is a valid expression (which evaluates to ) and so is:

… which evaluates to .

Note: I'm not going to spell out the rules for evaluation in this post since I'm just focused on explaining type inference.

The type inference rule for is the first time we'll actually use the context () that we've ignored so far:

This says that in order to infer the type of a expression we need to first infer the type of the right-hand side of each local variable assignment (, , …). We then extend our type inference context () with the inferred type of each assigned local variable () while inferring the type of the final result ().

data Expression =

    | Let [(Identifier, Expression)] Expression



infer context (Let [] expression) = do
    infer context expression

infer context (Let ((x, assignment) : assignments) expression) = do
    assignmentType <- infer context assignment
    
    infer ((x, assignmentType) : context) (Let assignments expression)

This rule pairs with the type inference rule for variables, which is:

… which you can read as saying "to infer the type of a variable named , look up 's inferred type in the context". If is missing from the context then this rule does not match and we reject the expression with a type error ("unbound variable"). The here is acting like a "named ellipsis" standing in for other inferred types that we don't care about right now.

data Expression =

    | Variable Identifier

infer context (Variable identifier) = do
    case lookup identifier context of
        Just assignmentType ->
            return assignmentType
        Nothing ->
            Left "unbound variable"

Armed with those two new rules we can now write out a typing derivation to infer the type of our earlier example:

This is essentially saying:

  • has type because…
    • has type because …
      • has type
    • … therefore has type
  • … therefore has type

… and we can confirm that in Haskell, too:

exampleLet :: Expression
exampleLet =
    Let [("r", Record [("x", Number 1)])]
        (FieldAccess (Variable "r") "x")
ghci> infer [] exampleLet
Right NumberType

Functions

If that's all our programming language needed then type inference for anonymous records would be easy and most statically typed programming languages would support anonymous records. However, every programming language also supports functions and that's where type inference begins to get tricky.

To see why, consider this TypeScript function:

const getName = person => person.name;

… which in the lambda calculus would be written as or in Nix would be written as:

getName = person: person.name;

What type would we infer for the function ?

In order to answer that question we need to first extend our syntax to use to denote an anonymous function:

… and we also need to add a new syntax for function types:

… where means a function whose input type is and whose output type is .

Then we can add a type inference rule for functions:

This is the first type inference rule we've written that can't just be directly translated to Haskell code as written (it is possible to codify using unification or similar, but that's outside the scope of this post). However, you can read that rule as saying that has type if has type when has type .

Even without code, we can still use that rule to reason through what the type for should be:

In other words, is a function whose input type is (a record with at least a field of type ) and whose output type is . That makes sense! So far, so good.

However, this is not exactly the type most programming languages would infer or expect. Specifically, most programming languages do not support an ellipsis like in a record type, but that's because there are two slightly better ways to handle types like that.

Subtyping

Some statically typed programming languages (like TypeScript) treat a record type like as a subtype of all larger records that share the same fields. In other words:

Under this approach the ellipsis is redundant because all record types have an implied ellipsis.

That means that in TypeScript you can write:

const getName = (person: { name: string }) => person.name;

… and getName will still work even if you call it on a larger record (e.g. { name: "Alice", age: 25 })5.

This approach works okay, but there is an even better approach for handling extra record fields, which brings us to:

Row polymorphism

The second approach is that instead of dropping the ellipsis we name the ellipsis, meaning that instead of this:

… we write something like this:

This is the approach taken by languages like PureScript and Elm, where PureScript will write that type like this:

{ name :: String | other }

… and Elm uses a similar syntax:

{ other | name : String }

This has some nice upsides, which we'll get to in the next section, but first let's formalize what it means to "name the ellipsis".

First, we'd create a new class of identifier (known as a "row variable"):

"Row" is an antiquated name for "a set of fields". Remember that the research into this sort of type inference occurred a long time ago, before JSON even existed. However, the upside of the name is that type theorists get to be a little cheeky and use the greek letter ("rho") to represent a "row".

Then we'd change the abstract syntax for record types to permit an optional row variable:

… and update our type inference rule for field access to use a row variable instead of an ellipsis:

This approach of using named ellipses is known as "row polymorphism" because it lets us abstract (be "polymorphic") over the set of other fields ("row").

Record extension

You might wonder: why do we want to abstract over the set of other fields? In particular, why do we need to give a name to these other fields?

One reason why is to support our next record operator: record extension. We'll use this syntax:

… which you can read as "extend the record with a field whose value is ". If the field already exists within then this extension replaces (a.k.a. "shadows") the old field.

So let's add that to our abstract syntax for expressions:

Now what type would we infer for a record update? Our first stab at type inference rule might look like this:

… but that is not correct because the field might already exist within the old record (), which means would include . That in turn means that is not necessarily a well-formed record type because an field included within would conflict with the we're adding to the final record type.

In order to fix this we need some notation that says that the old record may or may not have a field named :

… and now denotes all fields within the old record that were not . This fixes the problem because now both occurrences of cannot include the field .

You can think of as analogous to TypeScript's x?: T notation for the type of a field that might be absent.

With that type inference rule we can now infer the type of a function like this one:

… which is:

In other words, the inferred type:

… says that this is a function whose input is a record that may or may not have a field plus some other fields () and the output is a record with a field storing a plus those same other fields ().

This is an example where we don't want to use ellipses in place of . If we were to replace with ellipses:

… then the type would no longer convey that the two ellipses represent the same set of extra fields.

If you were to write the same function in TypeScript the type would be a lot clumsier:

function crown<T extends object>(woman: T): Omit<T, 'queen'> & { queen: boolean } {
    return { ...woman, queen: true };
}

… because TypeScript doesn't have a way to abstract over sets of fields (rows). TypeScript can only abstract over types (like T in the above example), which makes this sort of type more awkward.

Conclusion

I'm personally interested in row polymorphism for another reason: right now I'm working on a type checker and language server for Nix and one of the trickier parts of the language to type check is Nix's // operator (the "record concatenation" operator that combines two records). Row polymorphism is basically the only approach to type inference that handles that operator well, which is explained in the paper Type inference for record concatenation and multiple inheritance and that paper is going to be the basis of my next post.

In the meantime, though, if you liked this post then you might also like another post of mine, Datatype unification using Monoids, which presents a simple type inference algorithm for JSON-like data.

Appendix

Here's the fully worked Haskell program that combines all the prior snippets from the post:

{-# LANGUAGE OverloadedStrings #-}

module Example where

import Data.Map (Map)
import Data.Text (Text)

import qualified Data.Map as Map

type Identifier = Text

data Expression
    = Boolean Bool
    | String Text
    | Number Double
    | Record (Map Identifier Expression)
    | FieldAccess Expression Identifier
    | Let [(Identifier, Expression)] Expression
    | Variable Identifier

data Type
    = BooleanType
    | StringType
    | NumberType
    | RecordType (Map Identifier Type)

infer
    :: [(Identifier, Type)]
    -- ^ context, a.k.a. "Γ"
    -> Expression
    -- ^ input expression
    -> Either Text Type
    -- ^ output inferred type
infer context (Boolean _) = do
    return BooleanType

infer context (String _) = do
    return StringType

infer context (Number _) = do
    return NumberType

infer context (Record fields) = do
    fieldTypes <- traverse (infer context) fields
    
    return (RecordType fieldTypes)

infer context (FieldAccess expression field) = do
    expressionType <- infer context expression
    
    case expressionType of
        RecordType fieldTypes ->
            case Map.lookup field fieldTypes of
                Just fieldType ->
                    return fieldType
                Nothing ->
                    Left "missing field"
        _ ->
            Left "not a record"

infer context (Let [] expression) = do
    infer context expression

infer context (Let ((x, assignment) : assignments) expression) = do
    assignmentType <- infer context assignment
    
    infer ((x, assignmentType) : context) (Let assignments expression)

infer context (Variable identifier) = do
    case lookup identifier context of
        Just assignmentType ->
            return assignmentType
        Nothing ->
            Left "unbound variable"
  1. … the other big thing being good type inference for anonymous variants/unions (a.k.a. "polymorphic variants") ↩

  2. There isn't really an official or standard notation for this sort of thing. Type theory notation is something that authors just sort of make up as they go and while there are some common conventions there is still a lot of notational variation between authors. In other words, it's just vibes and as long as it's understandable in context that's usually good enough. ↩

  3. Haskell in shambles6

  4. There are some situations where it does make sense to interleave type checking and evaluation and I discuss one such case in another post of mine: Type-safe eval in Grace. ↩

  5. … with the exception of a fresh object, which would be rejected by the excess property checker. ↩

  6. Haskell does technically support accessing record fields nowadays using the OverloadedRecordDot language extension but it's still fairly unergonomic, in my opinion. I still love Haskell to death, though, as you might guess from the name of my blog. ↩