typed-process
documentationSummary: The Haskell package
typed-process
provides an API for launching and managing processes. It is more
type-safe and composable than its older cousin,
process
. In this
article I explain how I improved the
typed-process
documentation to make this library shine brighter! Hopefully the
techniques explained here can help other library authors with their
documentation too.
process
Haskell has a package called
process
for launching
and managing processes using the system’s underlying UNIX or Win32
API. Whenever I’ve used it I have found it to be a solid,
well-implemented library. On the other hand I always struggled to
piece together components to do anything more complicated than the
built-in functions like
callProcess
.
I haven’t found the library particularly composable.
The library does allow one to separate the configuration of processes
from launching them. Configuration consists in defining a value of
type
CreateProcess
which contains parameters like the executable file path, the
arguments, and determines what the standard input, output and error
streams should be. Once one has defined a CreateProcess
one can
launch a process by using functions like
readCreateProcess
which takes the process’s stdin
as a String
argument and returns
its stdout
as a String
(overriding any such settings you gave when
you made the CreateProcess
).
But there are also functions that configure and launch in one go, such
as
callProcess
.
callProcess
takes the executable path and list of arguments just
launches that process, no separate configuration step in sight. For
some reason I find the mixing of two-stage functions with one-stage
functions in the same API really hard to get my head around.
Beyond the mixing that confused me there is also the daunting monster
createProcess
, the most general way to launch a process.
createProcess :: CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle,
ProcessHandle)
I find it daunting mostly because of the three Maybe Handle
s (which
correspond to the process’s input and output streams). Firstly,
Handle
is a rather low-level concept. Secondly, each of those
Maybe
s is Just
if and only if the corresponding std_in
,
std_out
or std_err
field on the CreateProcess
is set to
CreatePipe
.
This invariant is documented on
createProcess
but it should be guaranteed by the type!
Whilst trying to get my head around how process
works I made a
couple of small
contributions[1][2]
to the documentation.
I recalled hearing that the
typed-process
package was a process API with better type-level guarantees so I
decided to check it out.
typed-process
My first foray into typed-process
ended almost as soon as it
started. I found the documentation more daunting than
createProcess
! You can look at the Haddock page as it was
then.
All the functions have documentation but they’re in one long, almost
uniform, list. Without additional structure I got lost. Furthermore
each type signature was on a long line which only wrapped at the end
of the line, for example
withProcessTerm_ :: MonadUnliftIO m => ProcessConfig stdin stdout stderr -> (Process stdin stdout stderr -> m a) -> m a
I am a strong believer that types are great documentation but the types need space to speak for themselves and here they were being suffocated. I couldn’t work out how to navigate.
To the credit of the package it says at the top
Please see the README.md file for examples of using this API.
and the README.md is a good introduction. But there was no link to take the reader there in one click. Little sources of friction like this are very discouraging to me when I am a new user. Small breadcrumbs to guide the way are very welcome!
process
Discouraged by typed-process
I returned to process
to try to work
out how to impose the type-level guarantees I wanted. Whilst
undertaking this work I significantly improved my understanding about
the internals of process
. Finally I worked out how to apply the
type-level guarantees I wanted by using a type-level Maybe
but the
change was such a large departure from the current API that I guessed
it would never be accepted.
typed-process
I decided to look more closely at typed-process
. I discovered that
it already had the type-level guarantees I wanted! Better than that,
it supports accessing the three standard streams in a well-typed way,
at higher-level types than Handle
, for example ByteString
. It
also firmly decides on a clear separation between configuring a
process (using
ProcessConfig
)
and launching it (with functions like
runProcess
).
The components that typed-process
provides compose together very
neatly. It makes life much more convenient than process
.
I was so impressed by the library that I decided to help improve the documentation in the hope of reducing the chance that someone will be discouraged from it like I initially was.
Below is the list of all improvements I made. You can see the
documentation before my
changes
and
afterwards.
Some of the changes (like “added an example”) are general to
documentation of any software package and some (like “explained
mkStreamSpec
) are specific to typed-process
.
Added a getting started example at the top of the page
The example (copied below) is very short but provides a simple way to get started.
{-# LANGUAGE OverloadedStrings #-}
runProcess "ls -l /home" >>= print
Linked to the README
The README, which contains a simple tutorial, was already mentioned at the top of the page but now it is a link. It might sound silly or minor but links like this really reduce cognitive load for someone coming to the library for the first time.
Forced type signatures to wrap
This is perhaps the single biggest readability improvement that I made. Instead of wrapping at the end of the line
withProcessTerm_ :: MonadUnliftIO m => ProcessConfig stdin stdout stderr -> (Process stdin stdout stderr -> m a) -> m a
the signatures now wrap at each argument
withProcessTerm_ :: MonadUnliftIO m
=> ProcessConfig stdin stdout stderr
-> (Process stdin stdout stderr -> m a)
-> m a
This format is achieved by adding dummy Haddock comments (-- ^
)
between each argument (see the commit in
question).
EDIT: Since GHC
9.0 you have to
use a space character after the ^
.
Moved the commonly-used functions higher than the less-used functions
For example mkStreamSpec
, with its terrifying type signature,
used to be documented at the top of the “Stream specs”
section
even though most users will only ever use the built-in
StreamSpec
s. Now it appears in a sub-section of its
own
beneath the built-in StreamSpec
s.
Similarly, startProcess
and stopProcess
used to be documented
at the top of the “Launch a process”
section
even though you’re not supposed to use them. You’re supposed to use
one of the withProcess...
functions instead. Even those
functions should be rarely used yet they took up positions 3-8. I
reordered
them
so that less-used functions appear lower down. The functions that
you are most likely to want, runProcess
and readProcess
, now
appear at the top of the
section.
Deprecated functions are not supposed to be used yet their documentation was wasting space slap-bang in the middle of functions you are supposed to use. I moved them to their own section right at the bottom of the module, out of sight, out of mind. Additionally, other parts of the documentation referred to use of the deprecated functions. I changed them to refer to the replacements instead.
Separated exception-throwing functions into their own section
Every process-launching function has two variants: a
non-exception-throwing variant and an exception-throwing variant.
The name of the latter differs from the name of the former by ending
in an underbar. The two variants used to appear next to each other
(see, for example, readProcess
and
readProcess_
). Since there is such a tight correspondence between the names and
functionality of the non-exception-throwing and exception-throwing
variants I decided to separate the exception-throwing functions into
their own
sub-section.
Subsequently it requires less scrolling to see all the process-launching
functions of the sort you want.
Clarified the mkStreamSpec
invariant
mkStreamSpec
is the only place where a user has to be aware of the
Just
/CreatePipe
invariant mentioned above in the discussion of
createProcess
. The old documentation used to mysteriously
say
“This will be determined by the StdStream argument”. I changed it
to mention the precise
invariant
and explained that the createProcess
documentation provides more
details.
Expanded the StreamSpec
documentation
It used to say
A specification for how to create one of the three standard child streams.
but I found that rather mysterious. What’s a “standard child
stream”? I added an
explanation
that it is one of stdin
, stdout
and stderr
, along with other
additional documentation.
Added internal links
The StreamSpec
documentation used
to
refer to the “Stream specs” section by saying “See examples below”.
I changed it to link
directly
to the “Stream specs” section. I added some other links too. The
links make it much easier to navigate around the page.
Arbitrary Haddock internal links are possible using a technique explained on StackOverflow by sjakobi.
Updated the links to the package and documentation home page.
It migrated to GitHub since the library was written.
typed-process
was always a great library but I couldn’t tell at
first because I couldn’t make my way into the documentation.
Hopefully my improvements make the library more accessible to
newcomers. typed-process
is designed such that the types are an
integral part of the documentation. My aim was to arrange the
documentation to allow the types to speak for themselves.
I would welcome your feedback on what I’ve done so far or
your suggestions for what to do next. Please contact
me.
Thanks to maintainer Michael Snoyman, and to all its other contributors, for this great library!