rio

repository·master·Indexed 21 days ago

https://github.com/commercialhaskell/rio

A Haskell standard library for production software development. It provides a curated set of trusted libraries, a safer Prelude replacement that removes partial functions and lazy I/O, and a collection of best practices for monad design, exception handling, and project structure. Includes the rio-orphans package for compatibility with common monad transformer typeclasses like MonadBase and MonadLogger.

Tokens
2.3K
Snippets
6
Records
12
Agent score
25%

What's inside rio

  1. Overview of the rio library

    master

    The rio library is designed to facilitate writing production-quality Haskell software. It serves three primary purposes:

    1. A collection of well-designed, trusted libraries: It standardizes on existing, commonly used libraries (like containers) and re-exports them to reduce dependency fragmentation.
    2. A Prelude replacement: The RIO module provides a more robust starting point than the standard Haskell Prelude, including common types like ByteString and Text while removing dangerous defaults like partial functions and lazy I/O.
    3. A set of best practices: It encourages a specific way of writing production Haskell code.

    For a detailed walkthrough, refer to the tutorial on how to use rio.

  2. Use orphan instances for the RIO data type

    master

    The rio-orphans package provides orphan instances for the RIO data type to enable compatibility with several common monad transformer typeclasses. This allows you to use RIO within larger application stacks that require these specific interfaces.

    Supported instances include:

    • MonadBase (from transformers-base)
    • MonadBaseControl (from monad-control)
    • MonadCatch and MonadMask (from exceptions)
    • MonadLogger (from monad-logger)
    • MonadResource (from resourcet)
  3. How to structure Monads using RIO

    master

    The rio library suggests a specific approach to monad design based on your needs:

    1. For I/O operations: Use the RIO monad. RIO is essentially ReaderT env IO but includes helpful utilities and better type signatures/error messages.
    2. For data access (The Has-pattern): Instead of passing a concrete environment to a function, use a typeclass constraint on the environment. This allows for better composability.
      • Bad: myFunction :: RIO Config Foo
      • Good: myFunction :: HasConfig env => RIO env Foo
    3. Using Lenses for Has-style classes: Use lenses (exposed by RIO) to implement these typeclasses. This enables easy composition of environments.
    4. For code usable outside of RIO: If you need to write general-purpose code, stick to standard mtl-style typeclasses like MonadReader, MonadIO, MonadUnliftIO, MonadThrow, and PrimMonad. Avoid MonadBase, MonadBaseControl, MonadCatch, and MonadMask.
    -- The recommended way to provide access to data via typeclass constraints
    class HasConfig env where
      configL :: Lens' env Config
    
    myFunction :: HasConfig env => RIO env Foo
    myFunction = do
      cfg <- view configL
      -- ... use cfg
  4. How rio standardizes the Haskell ecosystem

    master

    Instead of creating new, incompatible versions of existing data structures, rio attempts to define a 'standard library' by reusing and re-exporting established, widely-used packages.

    When you use rio, you are encouraged to use the types and functions it re-exports. This helps prevent the 're-implementation' problem where different libraries use incompatible types for the same task.

    Key Pattern: Most standard libraries integrated into rio are exposed via modules prefixed with RIO.. For example, if a library's primary interface is standardized, you will find its functions and types under a RIO.<PackageName> module.

  5. Using the RIO module as a Prelude replacement

    master

    The RIO module is intended to be used as your primary entry point, replacing the standard Prelude.

    What it provides:

    • Common data types out of the box (e.g., ByteString, Text).
    • A safer environment by removing common 'gotchas' such as partial functions and lazy I/O.

    Module Hierarchy Strategy:

    • RIO module: Contains functions and types that are safe to use in general and have no expected naming conflicts.
    • RIO.<Name> modules: Contains functionality that should not always be used by default or that has potential naming conflicts with the core RIO module.
  6. Execute the generated application

    master

    To run the application generated by the template, use stack exec with the executable name. The executable name follows the pattern {{name}}-exe, where {{name}} is the name of your project.

    To run with standard output:

    stack exec -- {{name}}-exe

    To run with verbose logging enabled:

    stack exec -- {{name}}-exe --verbose
  7. Recommended Language Extensions

    master

    The rio library recommends enabling a specific set of language extensions to improve safety and developer experience. It is recommended to add these extensions on-demand in individual source modules rather than in your package.yaml or .cabal files to avoid tooling issues.

    Recommended Defaults: 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.

    Note on OverloadedStrings: While it can break existing code, it is recommended to encourage the avoidance of the String type. Note on MonadFailDesugaring: This helps prevent partial pattern matches.

  8. Recommended Import Practices for RIO

    master

    To use rio effectively, follow these import patterns to ensure a consistent environment and avoid name clashes:

    1. Enable NoImplicitPrelude: This allows you to replace the standard Haskell Prelude with rio.
    2. Use import RIO: Add this as your primary replacement prelude in all modules.
    3. Qualified Imports for RIO Modules: Use the RIO.-prefixed modules with qualified imports as recommended in their documentation. For example, import qualified RIO.ByteString as B.
    4. Infix Operators: You may import infix operators unqualified if they do not cause name overlaps in your module. If they do overlap (e.g., importing both RIO.Map.\ and RIO.List.\), import them qualified.
    -- Recommended setup
    {-# LANGUAGE NoImplicitPrelude #-}
    import RIO
    import qualified RIO.ByteString as B
    import RIO.Map ((?!), (\)) -- Only if no name overlaps exist
  9. Exception Handling Best Practices

    master

    Follow these principles for robust error handling:

    1. Expected Failures: If a failure is expected and the caller should handle it every time (e.g., a lookup), return a Maybe or Either value.
    2. Unexpected Failures: For errors the user typically won't want to handle immediately, use exceptions.
      • In pure code: Use a MonadThrow constraint.
      • In IO code: Use runtime exceptions via throwIO (this works within the RIO monad).
    3. Resource Management: Always use functions like bracket and finally for resource allocation.
    4. App-wide Exceptions: Define a custom exception type for your application to provide meaningful error context.
    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."
  10. Recommended GHC Warning Flags

    master

    To catch potential problems, use the following GHC compiler warning flags. You can add these per file, to your package.yaml, or via the command line.

    Standard Warnings:

    • -Wall
    • -Wcompat
    • -Widentities
    • -Wincomplete-record-updates
    • -Wincomplete-uni-patterns
    • -Wpartial-fields
    • -Wredundant-constraints

    Production Recommendation: For production code, use -Werror to turn all warnings into errors, forcing resolution before shipping.