# Rio: a standard library and Prelude replacement for production Haskell

> Rio is a Haskell library that tries to be the standard library the ecosystem lacks. Rather than defining its own Map or its own IO, it reexports the trusted libraries others already use, exposes them under a RIO-prefixed module hierarchy, and ships a curated list of language extensions and compiler flags as recommended defaults for production code.

**commercialhaskell/rio** — A standard library for Haskell

- Repository: https://github.com/commercialhaskell/rio
- Stars: 868 · Forks: 58
- Language: Haskell
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/commercialhaskell-rio

## The problem Rio is trying to solve

The stated goal of the rio library is to make it easier to adopt Haskell for writing production software, and it describes itself as a cross between a collection of well-designed, trusted libraries, a useful Prelude replacement, and a set of best practices for writing production quality Haskell code.

The problem it diagnoses is a real one in Haskell. While GHC ships with a base library, as well as a number of other common packages like directory and transformers, there are large gaps in functionality provided by these libraries. The minimalism of base is by design, and the README spells out the consequences.

For a given task, it is often unclear which is the right library to use. When writing libraries, there is often concern about adding dependencies outside of base, because it creates a heavier dependency footprint. And by avoiding dependencies, many libraries end up reimplementing the same functionality, often with incompatible types and type classes, leading to difficulty using libraries together.

That third consequence is the one that hurts most in practice. Two libraries that each provide their own Map are not two libraries, they are one library and one obstacle. The README reaches for XKCD #927 to make the point.

Rio's answer is not to write the missing libraries. It is to pick the ones the community already agrees on and make them easy to reach.

## Reexporting rather than reinventing

The specific approach the README describes is that the library reuses existing, commonly used libraries. Instead of defining an incompatible Map type, it standardises on the commonly used one from the containers library and reexports it.

That single decision explains most of what follows. The library attempts to define a set of libraries as standard, meaning they are recommended for use and should be encouraged as dependencies for other libraries, and it does this by depending on those libraries itself and reexporting their types and functions.

The mechanics are straightforward. The list of packages considered standard is visible in the dependencies of the package, and the primary interfaces of each of those packages are exposed from rio through a RIO-prefixed module that reexports the interface. So a ByteString module, a Map module, a Text module, each wrapping the upstream package rather than replacing it.

The payoff for the user is on two levels. At the ecosystem level, a library author who depends on rio's Map is depending on the same Map everyone else uses, so the types unify. At the user level, a new team has one dependency to add for a large percentage of common functionality rather than a dozen decisions to make.

The risk on the ecosystem level is the mirror image, and it is worth naming. A library that reexports a dozen packages makes those dozen packages its dependencies, and any consumer of that library inherits them. That is why rio-orphans exists as a separate package in the same repository, and why the library is better suited to applications than to published libraries.

The repository also contains a tutorial on using rio on FP Complete's Haskell site, and the README positions itself as project goals and reference information rather than the tutorial itself.

## The RIO module and the rule behind it

The RIO module works as a prelude replacement. According to the README it provides more functionality and types out of the box than the standard prelude, such as common data types like ByteString and Text, and it removes common gotchas, like partial functions and lazy IO.

The guiding principle is stated as a two-part rule. If something is safe to use in general and has no expected naming conflicts, expose it from RIO. If something should not always be used, or has naming conflicts, expose it from another module in the RIO. hierarchy.

That rule is the whole design in one sentence, and it explains a lot about how the library feels in use. The top-level RIO module is the one you import everywhere and it is deliberately conservative, because anything in it is in scope in every module of your project. The rest of the hierarchy is where the specialised and the conflicting things live, and you import those qualified under the recommended names from the module documentation, for example importing qualified RIO.ByteString as B.

Removing partial functions is worth dwelling on, because it is the difference between a library that fails loudly and one that fails quietly. A partial function like head on an empty list throws an exception that surfaces far from the cause. Excluding those from the default scope means the compiler stops you at the call site instead.

Removing lazy IO is the other one, and it matters more than it sounds. Lazy IO has a long history of resource leaks and of a program holding open a file handle long after you thought you were done with it, and the decision to leave it out of the default scope means the safe version is the one you get by accident.

The NoImplicitPrelude language extension is part of this arrangement, and it is recommended rather than optional, which closes the loophole where the standard Prelude stays in scope and you shadow half of it.

## Import practices and the language extension defaults

The import practices section recommends enabling NoImplicitPrelude, adding import RIO as the replacement prelude in all modules, and using the RIO-prefixed modules as necessary with the qualified names from the module documentation.

There is one detail in there that causes real friction. Infix operators may be imported unqualified, but only if your module contains no overlapping infix names. The example given is RIO.Map's (?!) and (\): if you are importing both RIO.Map.\ and RIO.List.\, do not import either one unqualified.

The reason is the same on both sides of that rule. The RIO hierarchy reexports from packages that themselves define common operators, so two modules in the hierarchy can offer the same name with different meanings, and an unqualified import that resolves to the wrong one produces a type error at best and a silently different behaviour at worst. The rule trades a little convenience for certainty about what a symbol means.

The language extension recommendations are the other half of the package's value, and the criteria for including one are stated: well accepted in the community, causing little to no code breakage versus leaving them off, and generally considered safe. The recommended default list is long and specific, and it includes AutoDeriveTypeable, BangPatterns, BinaryLiterals, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, DoAndIfThenElse, EmptyDataDecls, ExistentialQuantification, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, InstanceSigs, KindSignatures, LambdaCase, MonadFailDesugaring, MultiParamTypeClasses, MultiWayIf, NamedFieldPuns, NoImplicitPrelude, OverloadedStrings, PartialTypeSignatures, PatternGuards, PolyKinds, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TupleSections, TypeFamilies, TypeSynonymInstances and ViewPatterns.

A separate list is given of extensions that are fine to use but are not recommended as defaults:

```text
CPP
TemplateHaskell
ForeignFunctionInterface
MagicHash
UnliftedFFITypes
TypeOperators
UnboxedTuples
PackageImports
QuasiQuotes
DeriveAnyClass
DeriveLift
StaticPointers
```

That list includes CPP, TemplateHaskell, ForeignFunctionInterface, MagicHash, TypeOperators, QuasiQuotes and DeriveAnyClass. The dividing line is roughly between extensions that change what a program means and extensions that change how it is written, though CPP and TemplateHaskell sit on the wrong side of that line by any reading, and the README says they are not recommended rather than forbidding them.

## The controversial entries, and where the README is honest about doubt

A recommendations list is only as good as its willingness to flag its own weak points, and this one has a notes section that does exactly that.

RecordWildCards is described as really up for debate. It is widely used, but is rightfully considered by many to be dangerous, and the README calls it an open question about what to do with it. The concern is real: the extension lets you write a pattern that binds a set of fields without naming them, which means adding a field to a record silently changes the meaning of existing code. It stays in the recommended list with the caveat attached, which is an honest way to handle a case where the community has not settled.

OverloadedStrings is recommended despite the acknowledgement that it can break existing code, on two grounds: it encourages avoiding the String data type, and for new code the risk of breakage is much lower. The second argument only applies to code you are writing now, which is why the caveat is there at all.

MonadFailDesugaring earns its place by preventing partial pattern matches, with an issue linked. That is the mechanism behind the removal of partial functions from the default scope: without the extension, a failing pattern match in a do block only surfaces as a runtime failure far from the cause, which is much harder to debug than a compile-time error.

The last item is about tooling rather than language. Due to concerns about tooling usage, with an issue linked, the recommendation is to add the extensions on demand in individual source modules instead of including them in package.yaml or.cabal files. The reasoning is that some tools read the cabal file and get confused by a long list of default extensions, so putting them per module keeps the tooling working at the cost of repeating them.

The GHC options recommendation is short and specific: -Wall, -Wcompat, -Widentities, -Wincomplete-record-updates and -Wincomplete-uni-patterns, offered as a set of warning flags to catch problems that might otherwise go unnoticed. Two of those five are about incomplete patterns, which is the same theme as MonadFailDesugaring and the removal of partial functions.

## Repository layout, the GHC matrix and what the licence situation is

The repository is a stack of resolver files, which is the clearest signal of what the project supports. There is a stack.yaml plus a stack.yaml.lock, and alongside them stack-ghc-8.6.5.yaml, stack-ghc-8.10.7.yaml, stack-ghc-9.0.2.yaml, stack-ghc-9.2.8.yaml, stack-ghc-9.4.8.yaml, stack-ghc-9.6.7.yaml, stack-ghc-9.8.4.yaml, stack-ghc-9.10.3.yaml and stack-ghc-9.12.2.yaml.

That is a matrix of GHC versions from 8.6.5 up to 9.12.2, each with its own resolver. The practice of shipping a resolver per GHC version rather than one resolver with a range is a Haskell community convention that comes from Stackage: a resolver pins the whole package set for one GHC version, and because the package set differs between GHC releases, supporting several GHC versions means supporting several complete dependency universes.

The consequences for a user are concrete. If you use stack, you pick the resolver matching your GHC. If you use cabal, you get the same effect through the index state.

The rest of the tree is small. There is a rio/ directory holding the library itself, rio-orphans/ for the related package, etc/ for other related libraries, and a set of configuration files for the linters, with an.hlint.yaml for hlint and a.yamllint.yaml for the YAML files the resolvers use. There is a CONTRIBUTING.md and a LICENSE file.

That licence file is worth a note. GitHub shows the licence as one it cannot classify, which is a different statement from the MIT-style licences many Haskell projects use. The README does not state the licence terms, so if you are considering a dependency in a commercial product, read the LICENSE file rather than assuming. It is a small thing to check and the kind of thing that is awkward to discover late.

The last fact is about the release process. The repository has no GitHub releases, and the last push was on 2026-07-03, three and a half months before the current date. The project is stable rather than actively churning, which for a standard library is what you want, and which also means the resolver matrix, not the commit log, is the thing to watch when you upgrade.

## Rio against a bare base dependency and against an alternative prelude

Two comparisons make the choice clearer for a team.

The first is against depending on base plus a handful of packages you picked yourself. That is what most Haskell projects do, and it is the approach Rio is arguing against. The argument is not that a curated list is better than an ad hoc one for a single project, because for a single project an ad hoc list is exactly as good. The argument is ecosystem-level: if everyone converges on the same Map, the Text and the ByteString, then libraries interoperate. A project that picks its own dependencies makes its own island.

The cost of that convergence is the dependency footprint. Any library that depends on rio inherits rio's whole set of upstream packages, which is exactly the concern the README attributes to library authors about adding dependencies outside base. Rio is therefore much more comfortable as an application dependency than as a library dependency, and a published library that wants to interoperate with everyone is better off depending on the individual packages directly.

The second comparison is against an alternative prelude, of which there are several. The difference is that Rio's primary identity is not the prelude module but the reexport scheme. An alternative prelude typically replaces Prelude with its own module and, if it goes further, exports its own versions of common types. Rio's stated approach is to not do that: it reexports upstream types and puts them in a namespace. The practical effect is that a Rio project and a non-Rio project can use the same Map, while a project using an alternative prelude that defines its own Map cannot.

There is also the middle option of doing neither and just being disciplined. A project can adopt the language extension defaults and the compiler flags without touching the Prelude, and get most of the benefit. What it loses is the single dependency that resolves the library-choice question and the removal of partial functions by scope rather than by discipline.

The decision usually comes down to team size and whether the code will be published. A team writing an internal service gains the most from standardising, since the people choosing libraries are a handful. A published library gains the least and pays the most in dependency footprint.

## Conclusion

Adopt Rio when you are starting a Haskell project and want a curated dependency set, a Prelude replacement without partial functions or lazy IO, and a shared set of language extension defaults your team can agree on. Do not adopt it in a library you publish, since a RIO dependency pulls a wide transitive set into your consumers, and expect to argue about RecordWildCards and OverloadedStrings, since the README itself calls those open questions. Verify first that the GHC version your project uses is covered by one of the stack resolver files, and that you are comfortable disabling the parts of the Prelude you have not explicitly reexported.

## FAQ

### What is the rio library for in Haskell?

The README states the goal is to make it easier to adopt Haskell for writing production software, and describes the library as a cross between a collection of well-designed, trusted libraries, a useful Prelude replacement, and a set of best practices for production quality Haskell code.

### How does rio avoid defining its own incompatible types?

It reuses existing, commonly used libraries. The README gives the example that instead of defining an incompatible Map type, rio standardises on the one from the containers library and reexports it, and the primary interfaces of each standard package are exposed through RIO-prefixed modules.

### Which language extensions does rio recommend enabling by default?

A long list is given as recommended defaults, including GADTs, RankNTypes, ScopedTypeVariables, OverloadedStrings, NoImplicitPrelude and RecordWildCards. A separate list covers extensions that are fine to use but are not recommended by default, such as CPP, TemplateHaskell, ForeignFunctionInterface and QuasiQuotes, and the README notes that the recommended ones are best added per source module rather than in the cabal file.

## Sources

- [commercialhaskell/rio on GitHub](https://github.com/commercialhaskell/rio)
- [Issues](https://github.com/commercialhaskell/rio/issues)
- [README](https://github.com/commercialhaskell/rio/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/commercialhaskell-rio
