Understanding PureScript’s Effect Rows for Safe Side‑Effect Management
Learn how PureScript’s Effect monad uses row‑polymorphic types to track side‑effects, why you need explicit effect quantification for higher‑order functions, and how to verify your effect handling in practice.
18 Dec 2025, 00:35 UTC

Why Effect Rows Matter
When you write PureScript code that needs to interact with the outside world—logging to the console, generating random numbers, or mutating a local reference—you still want the rest of your program to stay pure. The Effect monad (type Effect a) solves this by tracking which side‑effects a computation may perform in its type signature, using a row‑polymorphic effect type. This lets the compiler infer effects automatically while keeping pure code free of runtime overhead.
How Effect Rows Work
An effect row is a list of effect constructors, such as console.log or random. A function that only logs has type Effect Unit with an implied row containing just console.log. If you later add a random number generator, the inferred row expands to include random. Because rows are erased at runtime, there is no cost for pure code; the only runtime cost is the dictionary passing needed to resolve effect handlers, which is comparable to monad‑transformer overhead in other languages.
Worked Example: Logging and Random Numbers
Consider a small program that logs a greeting and then returns a random integer between 1 and 10.
module Main where
import Effect (Effect)
import Effect.Class.Console (log)
import Effect.Random (randomInt)
main :: Effect Unit
main = do
log "Hello, PureScript!"
n <- randomInt 1 10
log $ "Your number is: " <> show n
The main function has type Effect Unit. The compiler infers that its effect row contains both console.log and random because both primitives are used. If you comment out the randomInt line, the inferred row shrinks to only console.log, and the type signature remains valid without any change.
Higher‑Order Functions and Effect Quantification
When you write a function that takes an effectful action as a parameter, you must explicitly quantify the effect variable to avoid leaking unintended effects. For example:
twice :: forall eff. Effect eff => (Effect eff Unit) -> Effect eff Unit
twice action = action >> action
The forall eff binder tells the compiler that action may perform any set of effects represented by the row variable eff, and the resulting twice will have exactly the same row. Without the forall, the function would default to a closed row, causing type errors if the argument performed effects not present in that closed set.
Limitations and Practical Checks
One limitation is that effect rows are only checked at compile time; there is no runtime guarantee that a handler for a given effect is actually provided. If you forget to supply a handler (e.g., you run a program that uses random but never initialize the random generator), the program will fail at runtime with a missing‑instance error. A practical way to catch this early is to enable warnings for missing effect instances (-Wmissing-effect-instances in spago builds) and to test the compiled JavaScript with node or purs run to verify that all expected effects are present.
Actionable Closing
Start by adding explicit effect quantification to any higher‑order function that works with Effect. Then, whenever you introduce a new primitive effect, let the compiler infer the updated row—only adjust the type signature if you need to expose the effect publicly. Finally, enable missing‑effect warnings and run a quick smoke test after each change to ensure the runtime environment provides the necessary handlers. This approach keeps your PureScript code both safe and ergonomic while leveraging the zero‑overhead benefit of erased effect rows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.