Using Row Polymorphism in PureScript to Write Flexible Record Functions
Learn how PureScript’s row polymorphism lets you write functions that accept any record with the required fields, and how lenses extend this idea to nested data.
08 Jul 2025, 03:30 UTC

The problem: functions that need only part of a record
When you start modeling data with records, you often write functions that only care about a few fields. For example, a greeting function needs a name field but the rest of the record (age, address, etc.) is irrelevant. If the function’s type is too specific, you lose the ability to reuse it with records that have extra information.
Thesis: row polymorphism lets a function accept any record that contains at least the fields it mentions, while preserving the rest of the record unchanged.
How row polymorphism works
In PureScript a record type can be written as { field :: Type | r }. The pipe introduces a row variable r that stands for “zero or more additional fields”. The compiler treats any extra fields as part of r and guarantees they are passed through unchanged at runtime.
-- src/Greeting.purs
module Greeting where
import Prelude
import Effect (Effect)
import Effect.Console (logShow)
greet :: { name :: String | r } -> Effect Unit
greet record = logShow ("Hello, " <> record.name)
The type says: greet needs a record with at least a name :: String field; any other fields are captured by r. You can call it with a record that has more fields:
-- src/Main.purs
module Main where
import Prelude
import Effect (Effect)
import Greeting (greet)
main :: Effect Unit
main = do
greet { name: "Ada", age: 30, city: "London" }
greet { name: "Bob" }
Both calls compile; the generated JavaScript simply accesses the name property, ignoring age and city.
Lenses for deep, polymorphic access
When you need to get or set a field nested inside a larger state, row‑polymorphic lenses from purescript-profunctor-lenses let you write a single getter/setter that works on any record shape that contains the required path.
import Data.Lens (Lens', lens)
import Data.Lens.Internal (Lens)
import Data.Lens.Tuple (_1, _2)
-- A lens that focuses on the `name` field, regardless of other fields
nameLens :: Lens' { name :: String | r } String
nameLens = lens _name \_ r -> r { _name = _ }
where
_name = _1 <<< _2 -- simplified for illustration; actual definition uses the lens library
-- Usage: modify the name inside any record that has a name field
updateName :: (String -> String) -> { name :: String | r } -> { name :: String | r }
updateName f = over nameLens f
The same updateName works on { name: String, age: Int }, { name: String, address: String }, or any record that adds more fields.
Trade‑offs and practical tips
- Type inference: Heavy use of row variables can make inferred types less precise, sometimes requiring you to add explicit type annotations to help the compiler.
- Compile time: Large code bases with many row constraints may see longer build times because the solver has to unify more row variables.
- FFI safety: PureScript records become plain JavaScript objects; missing fields are
undefinedat runtime. Row polymorphism does not guard against absent keys—if you need a guarantee that a field exists, add a runtime check or use a newtype wrapper.
To verify that row polymorphism works in your project:
- Initialize a PureScript project with
spago init. - Add
purescript-prelude(and optionallypurescript-profunctor-lenses) viaspago install. - Create
src/Main.purswith thegreetexample above. - Run
spago build– compilation should succeed without type errors. - Execute the output with
spago run(ornode -e "require('./output/Main/main').main()") and observe the greetings printed to the console.
Actionable closing
Start by extracting the fields you actually need into a row‑polymorphic type signature. If you find yourself writing the same accessor for many record shapes, replace it with a lens. Keep an eye on compile‑time warnings and add explicit types when inference becomes unclear. This approach gives you reusable, safe code while preserving the flexibility of PureScript’s extensible records.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.