May 14, 2024Sönke Hahn, Alex David, Julian Arni

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 functionsshare

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:

descriptionprocesscradle
run a command, inheriting stdout/stderr, throwing an exception on non-zero exit codecallProcess "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.

Configurationshare

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 outshare

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

Mar 14, 2024Julian K. Arni

What happens if we make URLs immutable? A somewhat unusual idea that can substantially improve and simplify deployments.

Dec 19, 2023Alex David

Microsoft's LSP is great for text-editor diversity, but it is severely lacking in flexibility for project-specific configuration.

Dec 7, 2023Alex David

Release announcement for garn version v0.0.19, which includes the ability to import Nix flakes from the internet and from your repository.

View Archive
black globe

Say hi, ask questions, give feedback