Using Row‑Polymorphic Records in PureScript to Write Extensible Functions
Learn how to write a function that accepts any record with at least a name field using PureScript’s row‑polymorphic types, see a concrete example, and understand common pitfalls.
16 Mar 2026, 15:17 UTC

Write a row‑polymorphic record function
In PureScript a record type is structural: two records with the same field names and types are the same type, even if they are defined in different modules. By leaving the row open with a variable you can write a function that accepts any record that contains at least the fields it needs.
Example: a greeting function
greet :: forall r. { name :: String | r } -> String
greet r = "Hello, " <> r.name <> "!"
The type reads: for any row r, a record that has a name :: String field and possibly other fields described by r.
How it works
When the compiler sees { name :: String | r } it treats r as a tail of the record row. The function body can only use the name field because that is the only field guaranteed to exist. At the call site you may pass a record with extra fields, for example:
person = { name: "Ada", age: 30, city: "London" }
The call greet person type‑checks because the compiler can match person’s row to { name :: String | r } by choosing r = { age :: Int, city :: String }.
Common pitfalls
- Forgetting the row variable: writing
{ name :: String } -> Stringmakes the function closed; passing a record with extra fields then yields a "could not match row" error. - Assuming field order matters: records are unordered at the type level, but the compiler may reorder labels in error messages, which can be confusing when comparing types visually.
- Updating nested records:
r { age = 31 }creates a shallow copy; to change a field inside a nested record you must repeat the update syntax for each level, which is why lenses are often preferred for deep structures.
Checking the compiled output
PureScript’s default backend emits plain JavaScript objects for records. After building your project you can inspect the generated file to confirm this:
- Run the build command in your project directory (you need read/write access to the folder):
spago build - Open the output directory, e.g.
output/Main/index.js(adjust the path to your module). - Look for the compiled form of
greet; you will see something likefunction greet(r) { return "Hello, " + r.name + "!"; }, showing thatr.nameis a simple property access on a JS object.
If you see a different shape (for example, a constructor wrapper) you may be using a custom backend or an older compiler version.
Limitations and verification
- Record types are erased at runtime, so when you pass a record through the Foreign Function Interface you receive a plain JS object with no automatic validation of its shape. You must check incoming objects at the boundary (e.g. with
purescript‑foreign‑genericor manual checks). - The exact syntax for record conveniences (like
#fieldaliases) can change between compiler releases; consult the release notes for the version specified in yourspago.yamlor package set. - To verify that a function is truly row‑polymorphic, try the following in the REPL (
spago repl):
Definegreetas above, then call it with a record that has extra fields and confirm it returns the expected string. Next, change the type to{ name :: String } -> Stringand re‑load; the same call will now produce a type error.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.