The H2 Wiki


improving-the-typed-process-documentation

Improving the typed-process documentation

Summary: 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 Handles (which correspond to the process’s input and output streams). Firstly, Handle is a rather low-level concept. Secondly, each of those Maybes 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!

Back to 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.

Back to 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.

Improving the documentation

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.

Conclusion

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.

Acknowledgements

Thanks to maintainer Michael Snoyman, and to all its other contributors, for this great library!