Announcing cradle
A simpler, more composable Haskell process library
We're releasing today the cradle Haskell library. The single purpose of it is to run subprocesses more easily. For most Haskell developers, it might be already obvious why this is desirable: the process library, which is the de facto standard for these tasks, is quite unwieldy and low-level.
(The name cradle, incidentally, comes from the idea that this library is a healthy environment for growing child processes.)
A proliferation of functions
When running subprocesses, sometimes you want to read stdout but not stderr, sometimes stderr but not stdout, sometimes neither, sometimes both. Sometimes you want to check the exit code yourself; sometimes you want the library functions to throw on error. Sometimes you want to provide stdin as a string; sometimes as a handle.
In order to accomodate all these options, process (and typed-process) end up with a large number of different functions, each of which combines these needs. Of course, it isn't true that every configuration option is a separate function; but those that affect the types often are.
What cradle does (borrowing heavily from shake's cmd function, which was the original inspiration) is use a single function (a pair of them, more accurately) that is polymorphic on the return type. This way, you can just combine the features you need in the return type:
description | process | cradle |
---|---|---|
run a command, inheriting stdout/stderr, throwing an exception on non-zero exit code | callProcess "ls" [] | () <- run $ cmd "ls" |
run a command, reading stdout, stderr, getting exit code | (e, out, err) <- runProcessWithExitCode "ls" [] | (e :: ExitCode, StdoutTrimmed out, StderrTrimmed err) <- run $ cmd "ls" |
run a command, reading stderr, inheriting stdout, getting exit code | (_, _, Just h_err, _, ph) <- createProcess (proc "ls" [] { std_err = CreatePipe }
(err, exit) <- withAsync (hGetContents h_err) $ \a -> do
exit <- waitForProcess ph
err <- wait a
pure (err, exit)
| (e :: ExitCode, StderrRaw out) <- run $ cmd "ls" |
As you can see, whereas with process we have to hope some function does exactly what we want (and consult documentation to find it), or else fall off a cliff of complexity, with cradle functionality composes in an obvious and easy way. There are also no partial pattern matches that we know are safe but have to be partial anyhow, and no repetition of logic regarding handles on the left- and right-hand side of the <-.
In addition to a polymorphic return type, cradle opts for only having synchronous functions that run to completion. That is, there's no start (or with*) functions which allow you to start but not finish the process. The thesis is that you can already get that behavior by using concurrency primitives such as forkIO and async.
cradle also doesn't have any shell-based functionality. We believe these are rarely, if ever, the right thing to use.
Configuration
cradle is configured in one of two ways: via the output type, and via functions applied to the cmd (usually in postfix, with &).
StdoutTrimmed out <- run $ cmd "nix" & addArgs ["build", ".#"] & setWorkingDir "somedir"
You can create new instances of Output, and new ProcessConfiguration -> ProcessConfiguration functions. You could for instance define:
data Timing = Timing { userSeconds :: Int , systemSeconds :: Int , totalSeconds :: Int } deriving newtype (Eq, Show) instance Output Timing where configure = \p -> p { executable = "time" , arguments = executable p : arguments p , stderrConfig = stderrConfig p { capture = True } } extractOutput = \o -> case stderr o of Nothing -> error "impossible: stderr not captured" Just v -> <parse the `time` output>
Then, you can run things like:
main = do timings <- run $ cmd "ls" putStrLn $ "'ls' took " <> show (userSeconds timings) <> " seconds"
(Note that when writing instances of Output, such as above, you will not always have type safety. But these instances are rare — most of the time, you are using, rather than extending, the library.)
As you can see, Output instances can configure the command run, but only based on type, not value. This makes sense: we need to know ahead of the result what we're going to run, and the only part of the Output that we know ahead of time is it's type.
For configuring inputs with values, we use simple ProcessConfiguration -> ProcessConfiguration functions. An example:
addTimeout :: Int -> ProcessConfiguration -> ProcessConfiguration addTimeout seconds p = p { executable = "timeout" , arguments = (show seconds <> "s") : executable p : arguments p) } main = do -- runs 'timeout 30s ls' run $ cmd "ls" & addTimeout 30
Try it out
cradle is up on Hackage — give it a spin!
Though we've been using this library ourselves, this is the first public release of cradle, so the API may not be fully stable, and feedback is particularly welcome.
Continue Reading
We've added incremental compilation to garnix. In this blog, we discuss prior art on incremental compilation in Nix, and describe our own design.
A short note about custom typing for functions in Nix