Tell me more ×

Pipes are a really elegant, really simple version of iteratees. You can write pipe code very easily using the primitives await and yield. Paolo Capriotti extended the concept of pipes with guarded pipes, which uses the slightly more complicated tryAwait primitive, which allows a pipe to perform some finalization once the input flow runs out.

The guarded pipe implementation redefines await in terms of tryAwait:

await = tryAwait >>= maybe discard return

My question is this: if I write code targeted at the simpler Pipes implementation (using await and yield), using the same code, will it behave the same if I switch over to the guarded pipe implementation? In other words, can guarded pipe code be treated as simply a superset of pipe code, in terms of behavior?

share|improve this question
add comment

2 Answers

up vote 11 down vote accepted

This is Gabriel, who posted Pipes. I've been working with Paolo and we have a more elegant implementation in the works that is even more powerful and typesafe than his original proposal. The short answer to your question is that the final implementation is a superset of the original Pipes and you can write the same code as before with identical behavior and semantics.

I can even summarize it pretty simply here. Await and yield statements are the only ways a pipe can give up control, so we attach to each one a fallback if the upstream or downstream pipe terminates. The fallback downgrades a pipe permanently and it can no longer repeat the failed action. A failed await downgrades a pipe to a producer and a failed yield downgrades a pipe to a consumer. If a producer fails to yield or a consumer fails to await, they are downgraded to the base monad, which can no longer fail.

Consumers and producers are now separate types and the are not exposed. They are the same as the type pipe type except missing the Await or Yield constructor. This is necessary, at least for the producer type, since there is no input type for pipes that can prohibit await statements.

Await and yield statements default to termination as their fall back behavior, which is the same behavior as before. Await and yield will be typeclassed to work in downgraded states that support them. However you can now optionally provide your own fallback as soon as we come up with a sexier name than tryAwait or tryYield.

I have to still verify that Pipes still form a category with this extension, but it seems very likely. It's also 100% typesafe and uses the types to enforce the downgrade rather than booleans and a programmer proven invariant.

Edit: Some functioning code to whet your appetite (Checkout the "try" branch from the github repository to use the extension):

printer = forever $ await >>= lift . print

take' n = replicateM_ n $ await >>= yield

fromList' = mapM_ (yieldOr (lift $ putStrLn "Undelivered elements))

diagnose = forever $ do
    x <- awaitOr (lift $ putStrLn "Await failed")
    yieldOr (lift $ putStrLn "Yield failed") x

> runPipe $ printer <+< take' 3 <+< diagnose <+< fromList [1..10]
Yield failed
Undelivered elements
> runPipe $ printer <+< take' 10 <+< diagnose <+< fromList [1..3]
Await failed
share|improve this answer
add comment

The answer is yes.

With the current implementation of pipes on Hackage, an awaiting pipe terminates as soon as its upstream terminates. The same is true with guarded pipes if you use the await function. You just have the option of using tryAwait instead if you need to behave specially before termination.

Besides, "guarded pipe" is just a temporary name for the concept while we work out the best way to integrate their functionality. I don't think they're going to be released separately.

share|improve this answer
add comment

Your Answer


