rio
A standard library for Haskell
https://github.com/commercialhaskell/rio#readme
LTS Haskell 22.33: | 0.1.22.0 |
Stackage Nightly 2024-09-07: | 0.1.22.0 |
Latest on Hackage: | 0.1.22.0 |
rio-0.1.22.0@sha256:95fc3725a638bb7d6e11be7a2cf8d601097ebc8cb992661b211a6e36ed1da726,3669
Module documentation for 0.1.22.0
The rio library
A standard library for Haskell
The goal of the rio
library is to make it easier to adopt Haskell
for writing production software. It is intended as a cross between:
- Collection of well designed, trusted libraries
- Useful
Prelude
replacement - A set of best practices for writing production quality Haskell code
This repository contains the rio
library and other related
libraries, such as rio-orphans
. There is a tutorial on how to use
rio
available on FP
Complete’s Haskell site. This README discusses project goals and
collects other reference information.
Standard library
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. This choice for a
more minimalistic base
is by design, but it leads to some
unfortunate consequences:
- For a given task, it’s often unclear which is the right library to use
- When writing libraries, there is often concern about adding
dependencies to any libraries outside of
base
, due to creating a heavier dependency footprint - By avoiding adding dependencies, many libraries end up reimplementing the same functionality, often with incompatible types and type classes, leading to difficulty using libraries together
This library attempts to define a standard library for Haskell. One immediate response may be XKCD #927:
To counter that effect, this library takes a specific approach: it
reuses existing, commonly used libraries. Instead of defining an
incompatible Map
type, for instance, we standardize on the commonly
used one from the containers
library and reexport it from this
library.
This 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. It does this by depending on these libraries itself, and reexporting their types and functions for easy use.
Beyond the ecosystem effects we hope to achieve, this will hopefully make the user story much easier. For a new user or team trying to get started, there is an easy library to depend upon for a large percentage of common functionality.
See the dependencies of this package to see the list of packages
considered standard. The primary interfaces of each of these packages
is exposed from this library via a RIO.
-prefixed module reexporting
its interface.
Prelude replacement
The RIO
module works as a prelude replacement, providing more
functionality and types out of the box than the standard prelude (such
as common data types like ByteString
and Text
), as well as
removing common “gotchas”, like partial functions and lazy I/O. The
guiding principle here is:
- 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.
Best practices
Below is a set of best practices we recommend following. You’re
obviously free to take any, all, or none of this. Over time, these
will probably develop into much more extensive docs. Some of these
design decisions will be catered to by choices in the rio
library.
For Haskellers looking for a set of best practices to follow: you’ve come to the right place!
Import practices
This library is intended to provide a fully loaded set of basic functionality. You should:
- Enable the
NoImplicitPrelude
language extension (see below) - Add
import RIO
as your replacement prelude in all modules - Use the
RIO.
-prefixed modules as necessary, imported using the recommended qualified names in the modules themselves. For example,import qualified RIO.ByteString as B
. See the module documentation for more information. - Infix operators may be imported unqualified, with a separate import
line if necessary. For example,
import RIO.Map ((?!), (\\))
. Do this only if your module contains no overlapping infix names, regardless of qualification. For instance, if you are importing bothRIO.Map.\\
andRIO.List.\\
do not import either one unqualified.
In the future, we may have editor integration or external tooling to help with import management.
Language extensions
Very few projects these days use bare-bones Haskell 98 or 2010. Instead, almost all codebases enable some set of additional language extensions. Below is a list of extensions we recommend as a good default, in that these are:
- Well accepted in the community
- Cause little to no code breakage versus leaving them off
- Are generally considered safe
Our recommended defaults are:
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
ViewPatterns
Notes on some surprising choices:
RecordWildCards
is really up for debate. It’s widely used, but rightfully considered by many to be dangerous. Open question about what we do with it.- Despite the fact that
OverloadedStrings
can break existing code, we recommend its usage to encourage avoidance of theString
data type. Also, for new code, the risk of breakage is much lower. MonadFailDesugaring
helps prevent partial pattern matches in your code, see #85
Due to concerns about tooling usage (see issue
#9), we recommend
adding these extensions on-demand in your individual source modules
instead of including them in your package.yaml
or .cabal
files.
There are other language extensions which are perfectly fine to use as well, but are not recommended to be turned on by default:
CPP
TemplateHaskell
ForeignFunctionInterface
MagicHash
UnliftedFFITypes
TypeOperators
UnboxedTuples
PackageImports
QuasiQuotes
DeriveAnyClass
DeriveLift
StaticPointers
GHC Options
We recommend using these GHC complier warning flags on all projects, to catch problems that might otherwise go overlooked:
-Wall
-Wcompat
-Widentities
-Wincomplete-record-updates
-Wincomplete-uni-patterns
-Wpartial-fields
-Wredundant-constraints
You may add them per file, or to your package.yaml
, or pass them on
the command line when running ghc. We include these in the project
template’s package.yaml
file.
For code targeting production use, you should also use the flag that turns all warnings into errors, to force you to resolve the warnings before you ship your code:
-Werror
Further reading:
- Alexis King explains why these are a good idea in her blog post which was the original inspiration for this section.
- Max Tagher gives an in-depth overview of these flags, and more, in his blog post.
Monads
A primary design choice you’ll need to make in your code is how to structure your monads. There are many options out there, with various trade-offs. Instead of going through all of the debates, we’re going to point to an existing blog post, and here just give recommendations.
-
If your code is going to perform I/O: it should live in the
RIO
monad.RIO
is “reader IO.” It’s the same asReaderT env IO
, but includes some helper functions in this library and leads to nicer type signatures and error messages. -
If you need to provide access to specific data to a function, do it via a typeclass constraint on the
env
, not via a concrete env. For example, this is bad:myFunction :: RIO Config Foo
This is good:
class HasConfig env where configL :: Lens' env Config -- more on this in a moment myFunction :: HasConfig env => RIO env Foo
Reason: by using typeclass constraints on the environment, we can easily compose multiple functions together and collect up the constraints, which wouldn’t be possible with concrete environments. We could go more general with mtl-style typeclasses, like
MonadReader
orMonadHasConfig
, butRIO
is a perfect balance point in the composability/concreteness space (see blog post above for more details). -
When defining
Has
-style typeclasses for the environments, we use lenses (which are exposed byRIO
) because it provides for easy composability. We also leverage superclasses wherever possible. As an example of how this works in practice:-- Defined in RIO.Logger class HasLogFunc env where logFuncL :: Lens' env LogFunc class HasConfig env where configL :: Lens' env Config instance HasConfig Config where configL = id data Env = Env { envLogFunc :: !LogFunc, envConfig :: !Config } class (HasLogFunc env, HasConfig env) => HasEnv env where envL :: Lens' env Env instance HasLogFunc Env where logFuncL = lens envLogFunc (\x y -> x { envLogFunc = y }) instance HasConfig Env where configL = lens envConfig (\x y -> x { envConfig = y }) instance HasEnv Env where envL = id -- And then, at some other part of the code data SuperEnv = SuperEnv { seEnv :: !Env, seOtherStuff :: !OtherStuff } instance HasLogFunc SuperEnv where logFuncL = envL.logFuncL instance HasConfig SuperEnv where configL = envL.configL instance HasEnv SuperEnv where envL = lens seEnv (\x y -> x { seEnv = y })
-
If you’re writing code that you want to be usable outside of
RIO
for some reason, you should stick to the good mtl-style typeclasses:MonadReader
,MonadIO
,MonadUnliftIO
,MonadThrow
, andPrimMonad
. It’s better to useMonadReader
+Has
than to create new typeclasses likeMonadLogger
, though usually just sticking with the simplerRIO env
is fine (and can easily be converted to the more general form withliftRIO
). You should avoid using the following typeclasses (intentionally not exposed from this library):MonadBase
,MonadBaseControl
,MonadCatch
, andMonadMask
.
Exceptions
For in-depth discussion, see safe exception handling. The basic idea is:
- If something can fail, and you want people to deal with that failure
every time (e.g.,
lookup
), then return aMaybe
orEither
value. - If the user will usually not want to deal with it, then use
exceptions. In the case of pure code, use a
MonadThrow
constraint. In the case ofIO
code: use runtime exceptions viathrowIO
(works in theRIO
monad too). - You’ll be upset and frustrated that you don’t know exactly how some
IO
action can fail. Accept that pain, live with it, internalize it, usetryAny
, and move on. It’s the price we pay for async exceptions. - Do all resource allocations with functions like
bracket
andfinally
.
It’s a good idea to define an app-wide exception type:
data AppExceptions
= NetworkChangeError Text
| FilePathError FilePath
| ImpossibleError
deriving (Typeable)
instance Exception AppExceptions
instance Show AppExceptions where
show =
\case
NetworkChangeError err -> "network error: " <> (unpack err)
FilePathError fp -> "error accessing filepath at: " <> fp
ImpossibleError -> "this codepath should never have been executed. Please report a bug."
Strict data fields
Make data fields strict by default, unless you have a good reason to do otherwise.
Project template
We provide a project template which sets up lots of things for you out of the box. You can use it by running:
$ stack new projectname rio
Safety first
This library intentionally puts safety first, and therefore avoids promoting partial functions and lazy I/O. If you think you need lazy I/O: you need a streaming data library like conduit instead.
When to generalize
A common question in Haskell code is when should you generalize. Here
are some simple guidelines. For parametric polymorphism: almost
always generalize, it makes your type signatures more informative and
functions more useful. In other words, reverse :: [a] -> [a]
is far
better than reverse :: [Int] -> [Int]
.
When it comes to typeclasses: the story is more nuanced. For
typeclasses provided by RIO
, like Foldable
or Traversable
, it’s
generally a good thing to generalize to them when possible. The real
question is defining your own typeclasses. As a general rule: avoid
doing so as long as possible. And if you define a typeclass: make
sure its usage can’t lead to accidental bugs by allowing you to swap
in types you didn’t expect.
Module hierarchy
The RIO.Prelude.
module hierarchy contains identifiers which are reexported
by the RIO
module. The reason for this is to make it easier to view the
generated Haddocks. The RIO
module itself is intended to be imported
unqualified, with NoImplicitPrelude
enabled. All other modules are not
reexported by the RIO
module,
and will document inside of them whether they should be imported qualified or
unqualified.
Changes
Changelog for rio
0.1.22.0
- Expose
augmentPathMap'
0.1.21.0
- Fix minor bug in
augmentPathMap
on windows wrt #234 not adhering to case-insensitive semantics
0.1.20.0
- Export
UnliftIO.QSem
andUnliftIO.QSemN
inRIO
0.1.19.0
- Expose
fromLeft
andfromRight
0.1.18.0
- Add colours to the
LogOption
constructor #222
0.1.17.0
- Expose
Bifunctor
,Bifoldable
, andBitraversable
. - The
first
andsecond
functions exported byRIO
formerly originated fromControl.Arrow
. They now come fromBifunctor
.
0.1.16.0
- Expand the number of
microlens
functions exported by the RIO prelude. - Add new module
RIO.Lens
which provides the rest ofmicrolens
.
0.1.15.1
- Replace
canonicalizePath
withmakeAbsolute
#217
0.1.15.0
- Include source in log messages
0.1.14.1
- Support
unliftio-core
0.2
0.1.14.0
- Addition of
mkSimpleApp
- Addition of
lookupEnvFromContext
0.1.13.0
- Add
withLazyFileUtf8
- Add
mapRIO
- Add generic logger
- Add
exeExtensions
and improvefindExecutable
on Windows #205
0.1.12.0
- Add
logFormat
andsetLogFormat
forLogOptions
.
0.1.11.0
- Replace atomic and durable file writing functions with the ones from
unliftio
, see #167
0.1.10.0
- Relax a bunch of
RIO.File
functions fromMonadUnliftIO
toMonadIO
- Custom
Monoid
instance forUtf8Builder
that matches semantics of the derived one, but doesn’t break list fusion - Qualified import recommendations for
*.Partial
,*.Unchecked
,*.Unsafe
- Re-export
Data.Ord.Down
fromRIO.Prelude
- Addition of
RIO.NonEmpty
module - Addition of
RIO.NonEmpty.Partial
module - Export
NonEmpty
type and its constructor(:|)
from RIO.Prelude.Types - Fix handling of non-ASCII characters in
logSticky
- Deprecate
withProcess
andwithProcess_
, addwithProcessWait
,withProcessWait_
,withProcessTerm
, andwithProcessTerm_
0.1.9.2
- Bring back re-export of
Monad.fail
fromRIO.Prelude
.
0.1.9.1
- Remove accidental reexport of
Control.Applicative.empty
introduced in the previous release. - Functions from
Data.Data.Data
class are brought to the re-export list as well.
0.1.9.0
- Add
Prelude.Exit
to export lifted versions of the exit functions fromSystem.Exit
. - Re-export the
Control.Monad.State.State
andControl.Monad.State.StateT
types and related computation functions inRIO.State
. - Re-export the
Control.Monad.Writer.Writer
andControl.Monad.Writer.WriterT
types and related computation functions inRIO.Writer
. - Re-export
pred
,succ
inRIO.Partial
. - Add
Semigroup
andMonoid
instances forRIO
- Add the
Deque
double-ended queue data type - Re-export
Data.Map.Strict.toAscList
andData.Map.Strict.toDescList
fromRIO.Map
. - Re-export
Data.Sequence.Seq
fromRIO
. - Addition of
RIO.Prelude
module - Addition of
RIO.Prelude.Types
module - Re-export
zipWith
andrunST
fromRIO.Prelude
- Re-export
Exception
,MonadFail
,Typeable
andST
fromRIO.Prelude.Types
- Switch to
MonadFail.fail
fromMonad.fail
and re-exported it fromRIO.Prelude
0.1.8.0
- Re-export
Control.Monad.State.modify
,Control.Monad.State.modify'
andControl.Monad.State.gets
inRIO.State
0.1.7.0
- Addition of
textDisplay
toDisplay
class.
0.1.6.0
- Changed
logUseColor
to default toFalse
on Windows, even when verbose and on the terminal - Add
RIO.File
module which offers a family of file handling functions (withBinaryFileDurable
,withBinaryFileDurableAtomic
, among others.) with better durability and atomicity guarantees
0.1.5.0
- Re-export
Numeric.Natural.Natural
#119 - Re-export
Data.Functor.<&>
from GHC 8.4+, falling back local definition forbase < 4.11
#117 - Re-export
Data.Proxy.Proxy(..)
- Re-export
fromEnum
from RIO, exporttoEnum
,read
andfromJust
from RIO.Partial - Add
noLogging
function to skip logging on specific sub-routines - Re-export
Control.Category.>>>
0.1.4.0
- Add
Const
andIdentity
- Add
Reader
andrunReader
- Add instances for
MonadWriter
andMonadState
toRIO
via mutable reference #103
0.1.3.0
- Add
newLogFunc
function to createLogFunc
records outside of a callback scope - Allow dynamic reloading of
logMinLevel
andlogVerboseFormat
for theLogOptions
record - Add
foldMapM
- Add
headMaybe
,lastMaybe
,tailMaybe
,initMaybe
,maximumMaybe
,minimumMaybe
,maximumByMaybe
,minimumByMaybe
functions toRIO.List
module (issue #82) - Move non partial functions
scanr1
andscanl1
fromRIO.List.Partial
toRIO.List
(issue #82) - Add
SimpleApp
andrunSimpleApp
- Add
asIO
0.1.2.0
- Allow setting usage of code location in the log output
0.1.1.0
- Move some accidentally included partial functions
0.1.0.0
- Initial stable release
0.0
NOTE All releases beginning with 0.0 are considered experimental. Caveat emptor!