Suppose I want to model, using Haskell pipes, a Python
Generator[int, None, None]
which keeps some internal state. Should I be usingProducer int (State s) ()
orStateT s (Producer int m) ()
, wherem
is whatever type of effect I eventually want from the consumer?How should I think about the notion of transducers in pipes? So in Oleg's simple generators, there is
type Transducer m1 m2 e1 e2 = Producer m1 e1 -> Producer m2 e2
but I don't know what the analog is in pipes, because any
Proxy
objects that interact seem to rely on the same underlying monadm
, not switching fromm1
tom2
. See the Prelude functions, for instance.
I think I'm just misunderstanding something fundamental about the way pipes works. Thanks for your help.
CodePudding user response:
In pipes
, you typically wouldn't use effects in the base monad m
of your overall Effect
to model the internal state of a Producer
. If you really wanted to use State
for this purpose, it would be an internal implementation detail of the Producer
in question (discharged by a runStateP
or evalStateP
inside the Producer
, as explained below), and the State
would not appear in the Producer
's type.
It's also important to emphasize that a Producer
, even when it's operating in the Identity
base monad without any "effects" at its disposal, isn't some sort of pure function that would keep producing the same value over and over without monadic help. A Producer
is basically a stream, and it can maintain state using the usual functional mechanisms (e.g., recursion, for one). So, you definitely don't need a State
for a Producer
to be stateful.
The upshot is that the usual model of a Python Generator[int, None, None]
in Pipes
is just a Monad m => Producer Int m ()
polymorphic in an unspecified base monad m
. Only if the Producer
needs some external effects (e.g., IO
to access the filesystem) would you require more of m
(e.g., a MonadIO m
constraint or something).
To give you a concrete example, a Producer
that generates pseudorandom numbers obviously has "state", but a typical implementation would be a "pure" Producer
:
randoms :: (Monad m) => Word32 -> Producer Int m ()
randoms seed = do
let seed' = 1664525 * seed 1013904223
yield $ fromIntegral seed'
randoms seed'
with the state maintained via recursion.
If you really decided to maintain this state via the State
monad, the type of the Producer
wouldn't change. You'd just use a State
internally. The Pipes.Lift
module provides some helpers (like evalStateP
used here) to locally add a monad layer to facilitate this:
randoms' :: (Monad m) => Word32 -> Producer Int m ()
randoms' seed = evalStateP seed $ forever $ do
x <- get
let x' = 1664525 * x 1013904223
yield $ fromIntegral x'
put x'
Oleg's simple generators are entirely different. His producers and consumers produce and consume values only through monadic effects, and "monad changing" is central to the implementation. In particular, I believe his consumers and transducers can only maintain state via a monadic effect, like a State
monad, though I'd have to look a little more carefully to be sure.
In contrast, pipes
proxies can produce and consume values and maintain internal state independent of the underlying base monad.
Ultimately, the analog of Oleg's transducers in pipes
are simply the Pipe
s. Both consume values from a producer and yield values to a consumer. The monad changing in Oleg's transducers is just an implementation detail.