GithubHelp home page GithubHelp logo

rio's Introduction

The rio library

A standard library for Haskell

Rio

Tests

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:

XKCD Standards

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 both RIO.Map.\\ and RIO.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 the String 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 as ReaderT 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 or MonadHasConfig, but RIO 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 by RIO) 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, and PrimMonad. It's better to use MonadReader+Has than to create new typeclasses like MonadLogger, though usually just sticking with the simpler RIO env is fine (and can easily be converted to the more general form with liftRIO). You should avoid using the following typeclasses (intentionally not exposed from this library): MonadBase, MonadBaseControl, MonadCatch, and MonadMask.

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 a Maybe or Either 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 of IO code: use runtime exceptions via throwIO (works in the RIO 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, use tryAny, and move on. It's the price we pay for async exceptions.
  • Do all resource allocations with functions like bracket and finally.

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.

rio's People

Contributors

adituv avatar akhra avatar bodigrim avatar bonds avatar chrisdone avatar dbaynard avatar fabfianda avatar fosskers avatar hasufell avatar hololeap avatar int-index avatar jkachmar avatar k0te avatar kerscher avatar lehins avatar locallycompact avatar mgsloan avatar mpilgrem avatar ocharles avatar ony avatar qrpnxz avatar raoulhc avatar rkoeninger avatar robx avatar roman avatar sam-gronblom-rj avatar snoyberg avatar stevenxl avatar vaibhavsagar avatar waddlaw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rio's Issues

Consider deprecating "stdin" / "stdout" / "stderr" in favor of "askStdin" / "askStdout" / "askStderr"

One minor gripe I have with the System.IO API (and, by extension, UnliftIO.IO) is the existence of global handles for stdio. In some circumstances it'd be quite handy if these were overridable with a bracketlike function such as withStdin h f. This would allow for silencing or redirecting output from a subpart of the program.

Perhaps this sort of silencing / redirecting / filtering / reformatting is the domain of RIO.Prelude.Logger.
However, that doesn't really handle overriding stdin. I think it would be nice if it was recommended to instead use askStdin :: (MonadReader env m, HasStdio env) -> m Handle. So, RIO might still export stdin, but with a deprecation pragma saying to prefer askStdin.

Not really sure if this is worth it, because it would mean that everyone would need to have stdio handles in their reader environment. It would not be possible to fallback on stdin / stdout / etc, because HasStdio would provide lenses on the environment so that they can be modified locally.

After typing this up, I've realized that it does not really pass the test of "codifying existing common practice". However, this sort of layer is the place to address this if we want to have any hope of having the ability to locally override stdio.

putStrLn is missing

I thought to try out RIO by creating a new project with stack new riotest. I added the long list of language pragma to the project cabal file for both the library and executable subprojects. I added import RIO to app/Main.hs and src/Lib.hs. And then I tried to compile. And then I discovered there's no putStrLn. And then I pondered why.

I thought it could be because String is evil. But then why not putStrLn from Data.Text.IO? Could be naked IO is evil. I see there's a logging library in RIO, maybe I'm being encouraged to use that?

Anyhow, it was surprising to see the default template is broken when using RIO, and it was surprising that putStrLn wasn't included. I guess I'm somewhere between a beginner and intermediate Haskeller. I can get hello world going, but I only understand Monads every once in a while, and only for a few hours at a time. ;) Just offering that as context as you ponder what part of your potential audience to focus on at this point.

I imagine you'll address putStrLn in the docs at some point, or maybe when you add a stack template, app/Lib.hs will use Logger with a comment telling folks that putStrLn is lame and you should really be using a logging framework if you want to output stuff to the terminal. Or maybe I'm guessing wrong.

Where should I ask question related to RIO?

I would like to use RIO for my next project (nothing serious, I am Haskell beginner) and I expect I will hit some difficulties on my way (maybe trivial).

As an example I am trying to parse new file format. But first I need to reverse engineer its structure. Therefore I tried to load the whole file into Bytestring.Lazy (it is big) and split the file into ascii text and binary nonsense (which I have to figure out what is it for). So I tried using dropWhile isAscii, takeWhile isAscii but I need to be able to compare Word with Char. Sadly RIO does not export fromEnum nor Bytestring.Char8 which would allow me to do this, so I ask myself if it is intentional or not, and whether my approach is even right.

I suspect Github issues are not the right place to ask. So I wonder if you use discourse or some similar chat app.

RIO.Text.removeSuffix / RIO.Text.removePrefix / RIO.List.stripSuffix

I noticed stripCR in the prelude and thought it was rather specific. I'd prefer removing it and instead having the following in RIO.Text:

removeSuffix s t = fromMaybe t (stripSuffix s t) 

Then stripCR is just removeSuffix "\r".

I also often have wanted:

stripSuffix :: [a] -> [a] -> Maybe [a]
stripSuffix s x = fmap reverse . stripPrefix (reverse s) . reverse

I suppose it doesn't exist because it's real inefficient. Should it be added with a warning about efficiency?

A bit off topic: I was surprised to see that there is no RULES pragma in base for reverse (reverse x) = x.

Rewrite export statements for nicer Haddocks

Currently, we get basically no exposed documentation. We need a rewrite that looks like this:

diff --git a/src/RIO/Prelude.hs b/src/RIO/Prelude.hs
index 09accaf..c8f6b91 100644
--- a/src/RIO/Prelude.hs
+++ b/src/RIO/Prelude.hs
@@ -39,11 +39,18 @@ module RIO.Prelude
   , writeFileDisplayBuilder
   , hPutBuilder
   , sappend
+  , Control.Applicative.Alternative
+  , Control.Applicative.Applicative (..)
+  , Control.Applicative.liftA
+  , Control.Applicative.liftA2
+  , Control.Applicative.liftA3
+  , Control.Applicative.many
+  , Control.Applicative.optional
+  , Control.Applicative.some
+  , (Control.Applicative.<|>)
   ) where
 
-import           Control.Applicative  as X (Alternative, Applicative (..),
-                                            liftA, liftA2, liftA3, many,
-                                            optional, some, (<|>))
+import qualified Control.Applicative
 import           Control.Arrow        as X (first, second, (&&&), (***))
 import           Control.DeepSeq      as X (NFData (..), force, ($!!))
 import           Control.Monad        as X (Monad (..), MonadPlus (..), filterM,

Unexpected spacing when using `withStickyLogger`

I noticed a little weirdness when using withStickyLogger. The spacing before the LogStr behaves unexpectedly.

Here's some example code:

module Main where

import RIO

main :: IO ()
main = runRIO env $ do
  logInfo "info"
  logOther "other" "thing"

data Env
  = Env
    { envLogFunc :: !LogFunc
    }

instance HasLogFunc Env where
  logFuncL = to envLogFunc

env :: Env
env = Env { envLogFunc }
  where
  envLogFunc :: LogFunc
  envLogFunc cs source level str =
    withStickyLogger options $ \f -> f cs source level str
    where
    options :: LogOptions
    options = LogOptions
      { logMinLevel = LevelDebug
      , logTerminal = False
      , logUseColor = False
      , logUseTime = False
      , logUseUnicode = False
      , logVerboseFormat = True
      }

Will output:

[info] info
@(src/Main.hs:7:3)
[other]  thing
@(src/Main.hs:8:3)

The LevelInfo output seems correct–there is one space before the LogStr–but the LevelOther output seems incorrect–there are two spaces before the LogStr.

If you turn off verbose (logVerboseFormat = False), the output has one extra space at the beginning of the line:

 info
 thing

The extra space when logVerboseFormat = False seems to come from this line:

" " <>

The extra space for LevelOther when logVerboseFormat = True seems to come from this line:

"] "

Seems like we could drop the space on line 208 and fix the problem if getLevel always appended a space for all the levels:

rio/src/RIO/Logger.hs

Lines 241 to 249 in 0bbdf3b

LevelDebug -> ansi setGreen <> "[debug]"
LevelInfo -> ansi setBlue <> "[info]"
LevelWarn -> ansi setYellow <> "[warn]"
LevelError -> ansi setRed <> "[error]"
LevelOther name ->
ansi setMagenta <>
"[" <>
display name <>
"] "

Seem fair/want a PR?

ImplicitParams

ImplicitParams is selected so that HasCallStack can be used for better error messages

ImplicitParams are not needed to use the HasCallStack feature.

Recommend MonadFailDesugaring?

I haven't used this extension myself previously, so my understanding may not be perfect. Consider this script:

#!/usr/bin/env stack
-- stack --resolver nightly-2018-04-15 script
{-# LANGUAGE NoImplicitPrelude #-}
-- {-# LANGUAGE MonadFailDesugaring #-}
{-# OPTIONS_GHC -Wall -Werror #-}
import RIO

main :: IO ()
main = runRIO () $ do
  Just x <- return Nothing
  return x

As is, it will crash at runtime with:

Main.hs: user error (Pattern match failure in do expression at /Users/michael/Desktop/Main.hs:10:3-8)

Note that even with -Wall -Werror turned on, the compiler does nothing to prevent us from the partial pattern match Just x <- return Nothing. By contrast, if we uncomment the {-# LANGUAGE MonadFailDesugaring #-} line, we get a compiler time error due to a missing MonadFail instance instead:

/Users/michael/Desktop/Main.hs:10:3: error:
    • No instance for (Control.Monad.Fail.MonadFail (RIO ()))
        arising from a do statement
        with the failable pattern ‘Just x’
    • In a stmt of a 'do' block: Just x <- return Nothing
      In the second argument of ‘($)’, namely
        ‘do Just x <- return Nothing
            return x’
      In the expression:
        runRIO ()
          $ do Just x <- return Nothing
               return x
   |
10 |   Just x <- return Nothing

It seems preferable to me to totally block partial pattern matches via this language extension and lack of MonadFail instance for RIO.

Consider adding RIO.Char

Simply to allow prescribing a qualified import, as Data.Char has several conflicts with Data.Text.

There's an argument that since everything else is qualified. Char doesn't have to be; but it seems better to be explicit. If I just see toUpper, I have to go to the imports list to really be sure I haven't simply forgotten to qualify Text. C.toUpper is unambiguous.

Add module RIO.STM and RIO.Concurrent?

I've noticed some of the Control.Concurrent.STM symbols are already given in RIO, however, others are missing (e.g. retry).

As well as STM, RIO does not export myThreadId nor threadDelay, should we provide explicit modules for those, or just include them as re-exports in RIO?

Provide a sibling package for console applications

Many things are only needed once you write a console application, but become almost always imported. This means probably a rio-executable, rio-executable-unix, etc with:

  • optparse-applicative
  • POSIX error codes and signals in scope
  • Conduit

Since it’s almost expected you will either be streaming to-and-from stdout, stderr and stdin and should play nice with the calling processes and shells reporting return codes.

Capitalization bikeshedding!

Somewhat spun off from #12.

We have rio the library, RIO the data type, RIO the module hierarchy. Do we want all of those capitalization schemes? rio lower case for the library seems pretty obvious, and the other two must begin with a capital letter. But do we choose RIO or Rio. Or perhaps they should be different? After all, the name of the library is not really based on the RIO data type any more, but is its own moniker.

I'll give some arguments in comments below, just framing the discussion here.

Lenses?

What support, if any, should we provide for lenses? Should we provide the implementation in rio itself, or import it from a package like microlens?

Should the whole `UnliftIO` module be re-exported?

My review of the re-exports that come from UnliftIO:

  • Control.Monad.IO.Unlift seems good 👍

  • UnliftIO.Async might be ok to re-export. It's the current defacto standard, sure, but some functions like wait / poll / link seem likely to accidentally alias other things. Has quite a lot of functions that don't get a ton of use but maybe that's ok.

  • UnliftIO.Chan does not seem like a good thing to re-export (see relevant section of https://lorepub.com/post/2016-12-17-Haskell-Pitfalls - I googled around a little for a concrete reason backing why I felt like channels weren't so great)

  • UnliftIO.Exception seems alright 👍 . Might consider leaving out impureThrow / assert, but they're probably ok considering that error is also exported by RIO.

  • UnliftIO.IORef 👍

  • UnliftIO.MVar 👍

  • UnliftIO.STM is mostly good, but it might make sense to omit TChan / TQueue. Why? They don't seem to get used a whole ton, seems prudent to remove exports when they aren't likely to be used much. Similar pitfall for channels applies to all but the TBQueue.

  • UnliftIO.Temporary 👍 Wish we could use path, though!

  • UnliftIO.Timeout 👍

Defining MonadCatch etc. instances for RIO?

I see that there's already a MonadThrow instance. Wonder if there's something fundamentally preventing defining MonadCatch, MonadMask instance for RIO? So that code written against those generic constraints could still be run in RIO.

There's a specific usecase I'm interested in where this is not a philosophic difference, that is where one can't just use RIO all the way down: for concurrency testing using dejafu, for which computations need to live in MonadConc from concurrency package.

Thank you.
+@barrucadu

Clean up RIO.Process

The naming is inconsistent, and just developed over time with Stack. Redesigning the API should make the module much nicer to work with. Also, providing an explanation for why this module exists in addition to System.Process.Typed is a good idea.

Data structure typeclasses or not?

Both foundation and mono-traversable have created sets of typeclasses for representing various data structures (sequences, maps, sets). The question is: should rio do this too?

Advantages

  • The prelude will be more useful, working on many different data types
  • It may make it easier to support new data types in the future

Disadvantages

  • Adding an extra dependency on mono-traversable (alternatively: move those typeclasses into rio and have mono-traversable depend on rio instead)
  • It seems like the general "best practice" in the community is to use qualified imports, and we may not want to buck that trend here

Regardless, I believe we should provide the RIO.ByteString, RIO.Text, etc re-export modules for those who want qualified imports, or for cases where the typeclasses would get in the way.

Recommended flags?

This article got me wondering what y'all think of:

  • -Wall
  • -Wcompat
  • -Wincomplete-record-updates
  • -Wincomplete-uni-patterns
  • -Wredundant-constraints

possibly in the (future) template, in the package.yaml.

How to do console I/O?

Lots of options here, focusing on writing:

  • String based API
  • Text based API
  • Builder-based API
    • Assume UTF8 everywhere
    • Fancy logic to determine encoding to convert to
  • Perhaps import the entire say package
  • Maybe define a new Display typeclass and/or helper functions to convert to Builders
  • Apply similar principles to RIO.Logger, or perhaps force all output via a RIO.Logger API (probably a bad idea)

For reading: Builder is out, so it's basically guaranteed to be a Text-based API (no one wants String, right?). Big question is whether we assume UTF8 or guess character encoding (assuming the guessing).

Qualified imports proposal

I propose we take the braindead simple approach of:

import Rio
import qualified Rio.Exe as Exe
import qualified Rio.List as List
import qualified Rio.Vector as Vector
import qualified Rio.Text as Text

(The rule being Rio.<foo> is qualified as <foo>).

Where Rio exports in the namespace the names of types, and generic functions like fmap and (<>), but functions/values are qualified. Lazy versions of things can have an alias like LazyText or unboxed things like UnboxedVector.

I can add support to Intero that will for example just automatically import import qualified Rio.Text as Text to your imports list if you write e.g.

main = writeUtf8 (Text.intercalate ", " xs)

So while the list grows, if it's automatically managed, it comes for free. So you wouldn't actually write your own import lists, the editor would do it.

Move / expose some RIO.Prelude.* modules

In PR #72, I propose to not export all the modules under RIO.Prelude.*. However, in some cases you might not want to import RIO, yet you still want to use some stuff from it without import lists. I'm thinking the following are good candidates for renaming and inclusion in exposed-modules:

  • RIO.Prelude.Display -> RIO.Display
  • RIO.Prelude.Logger -> RIO.Logger
  • RIO.Prelude.RIO -> RIO.Monad? Iffy. RIO.RIO seems a little overexcited about RIO.
  • RIO.Prelude.Trace -> RIO.Trace
  • RIO.Prelude.URef -> RIO.URef? Also a bit iffy since there isn't a RIO.IORef, for example.

So, this would leave the following modules under RIO.Prelude.___, and not included in exposed-modules: Extra, IO, Lens, Reexports, Renames, and Text.

Will RIO include visualisation library?

When prototyping I often want to reach for quick and easy visualization. There are areas where visualization is strictly better than ascii output and then there are areas where good visualization is irreplaceable.

Are there any plans on including some lightweight, simple plotting and/or drawing library in RIO?

Consideration to include a Pretty API

Hi, I'm wondering if there is a particular reason why a Pretty API is not "batteries included" in RIO?

I understand there are many to choose from:

I would like to get an informed opinion on why one or the other, are RIO maintainers open to start a conversation about options and eventually include one?

Depend on conduit?

Some of the support code pulled out of Stack provides conduit helper functions. Should we keep a dependency on conduit in rio, or rather drop it?

Depend on clock?

We can probably provide some very useful helpful functions, like logTimed. We already do stuff like this in Stack for running external processes, and can generalize it.

Prefer () / Void when appropriate?

Currently, forever :: Applicative f => f a -> f b, however I think a better type might be forever :: Applicative f => f () -> f Void. Why is this better?

  1. The typechecker will let the user know that they are ignoring the result value of the argument to forever.

  2. The typechecker lets you know that it will never return. Instead of "Sure, I'll return an Int!"

This can be recognized fairly mechanistically, I think. Getting real formal on y'all:

  • Covariant type variables that are only used once and have no constraints are isomorphic to ().

  • Contravariant type variables that are only used once and have no constraints are isomorphic to Void.

For type variables like these, I think the better choice is usually to go with the concrete type.

Some reasons not to do this:

  1. Making it a bit harder to port things to rio.
  2. Void and absurd are potentially confusing to newcomers, because void in many languages is closer in meaning to (). It's also weird that we also have void :: Functor f => f a -> f ()...

First stable release

I think we're at a point where we can make a first stable release of the library. I believe it should be versioned at 0.1.0.0, and the goal will be to avoid breaking changes from that point onward. (All releases to date have been marked as experimental and have massively broken the API.)

I'm opening this issue to alert people about this, and give everyone a chance to get some PRs in. I believe we need to resolve the following issues before a release:

  • #12 (qualified imports): it looks like the short names overall is more popular, so this is mostly a matter of updating the relevant modules with the right documentation
  • #31 (trace functions): the biggest issue here is figuring out what the type of these functions should be (String, Text, etc)
  • #43 (remove partial functions): I'm not convinced we'll have a complete removal of all partial functions, but there is likely some low-hanging fruit to attack, like head, tail, etc
  • #51 (better Display typeclass)

Post release, I'd like to create a Stack template for rio.

More informative documentation for RIO module

From a documentation perspective, it is quite hard for a user to tell what the RIO module exports. This is because it uses module exports. The re-export depth can be quite high. For example, RIO exports module RIO.Prelude.Reexports, and it exports module UnliftIO, which itself consists of module exports. From the perspective of the compiler or heavy ghci user that just uses :bro RIO (:browse), this doesn't obscure anything at all. However, for a reader of the documentation, it can be hard to tell what RIO really provides.

I started improving the readability of the haddocks for RIO.Prelude.Reexports - https://gist.github.com/mgsloan/bec603328664ea7d20075fdddefad5a0 , but I started wondering if it would be better move it into the RIO module and switch everything over to explicit exports. There are some downsides to this idea, though. One of them is that it may be easy to add something and forget to add it to the re-exports.

So, it seems like the best option would be to improve haddock to allow some annotation on module re exports which causes them to be expanded.

Now, a fair question would be whether we really want that - the full docs for the RIO module might be overwhelming particularly with the large instance lists for many of the classes. I think it'd be more helpful than harmful, though. To me it makes sense to be able to read the full docs for your prelude in one page, and also be able to use browser text search within that page.

Add a `ToFilePath` typeclass of some sort?

Not a fully formed idea, but basically: define

class ToFilePath path where
  toFilePath :: path -> FilePath
instance Char ~ a => ToFilePath  [a] where
  toFilePath = id

Then functions like writeFile can work on this typeclass instead of just FilePath, making it easier to start using alternatives (like @chrisdone's path package). Downsides:

  • The dep tree to avoid orphan instances is unclear
  • It breaks type inference for string literals if OverloadedStrings is turned on, e.g. writeFile "foo.txt" "bar"
  • This doesn't fit in perfectly with matching existing best practices

Offer durable file writing functions

rio should offer file writing functions that are durable.

That involves fsync()ing the file itself after writing and before close(), and fsync()ing the directory that contains the file. It also involves failing hard on errors in fsync(), as retrying fsync has no effect.

Sources for fsyncing the directory:

See also:

Note it may not be possible to provide durable variants for all operating and file systems, but these functions should be as durable as they can be.

We should also discuss whether durability should be the default in writeFileBinary and writeFileUtf8.

In general, rio should expose durable and non-durable variants of file writing operations.

Namespace conflicts between RIO and RIO.Map

RIO exports Data.Foldable.null. RIO.Map (implicitly) exports Data.Map.Strict.null. Being not technically the same function (despite the former being defined as the latter), they conflict. There are quite a few other examples of this (e.g. most of Foldable) -- null is just the first one I ran into.

This comes down to base Data.Map being intended for qualified import, so they didn't use unique names; whereas Rio doesn't suggest a default qualifier, so I assume it's intended for use without one. On the upside, the fix should be simply hiding the conflicts in RIO.Map's import.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.