-
Notifications
You must be signed in to change notification settings - Fork 525
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WriterT[IO.. losing logs in parEvalMap hints at problem with Concurrent instance for WriterT ? #3764
Comments
See some related discussion in typelevel/fs2#3246 (comment). tl;dr don't use monad transformer datatypes. You can replicate |
I want to respond on two levels..
There's a few options that IMO are OK
..and one that is kind of bad:
Now I can't actually envisage why WriterT couldn't be supported for parEvalMap by monoidally combining the logs from the various concurrent fibers. I'll bet there's a fixable bug somewhere. There seems a tendency to bucket anything MT-related together, but it's more nuanced than that. There's a class of MTs like EitherT that are short-circuiting and seem to be problematic with the CE error channel, but WriterT isn't of that form. When I inquired about this issue on TL Discord, Fabio's response actually pertained to a different situation, around error-handling, where WriterT drops logs on error (I actually believe the correct way to handle that is to raise an error of the form Level two ,"Why are MTs worth bothering with anyway?" is more ideological and will come in a second comment.. |
Level 2: Why are MTs worth bothering with anyway? I retain an interest in monad transformers not because I love them in themselves - actually they are a confusing pain - but because I retain a naive but as yet unshaken belief in simple functional programming. By which I mean Runar's definition of functional programming, namely "programming with functions". Pure mappings whose only effect on the world is through their return type. In the world of pure functions, state dependence manifests via a type signature like (A, S) => (B, S), logging via a signature like (A) => (B, L), errors via (A) => Either[E, B]. These kinds of pure signatures are, to me at least, intuitive, easy to reason about, and easy to test. Ease of testing remains one of the most compelling practical advantages of FP. I want to be able to write the computational pieces of my program using forms like those above. Monad Transformers let me write simple, pure code in the small, and yet lift it into a larger effectful program fairly gracefully. It does not seem as nice to carry around a mutable Ref side-channel to store state or logs in. Pure functions become effectful. Type signatures start to "lie", in that they now omit some of the effects of a function. Asserting in a test becomes less simple. |
I tried to make this point several times in the discussion you linked, but I think I've failed to convey it: in that world, you cannot use real concurrency nor native errors, so it's incompatible with
I was actually trying to explain that it's a simpler case of the same issue: you need native mutable state, and |
So a design goal I'm seeking for is that the individual pieces of (non-concurrent, non-exception-throwing) domain logic in my program are phrased as pure functions with simple signatures like above. What I think you're pointing out is that, as the program is composed & layered, as it scales up, as concurrency, 3rd party libs, io, and inevitably exceptions get involved, these simple forms aren't sufficient any more. And At the moment I'm thinking about ways to transition from logging as |
Stepping back a bit though, I do think this issue and others that have appeared in the last year or so are unsettling. If you read the blurb on CE3 it says something like:
And have an implementation of That's troubling other people than me, right..? Where's the gap..
|
I'm a broken record, but I highly recommend to use the https://typelevel.org/cats-mtl/mtl-classes/tell.html It abstracts the main idea of
IMHO a signature written in terms of
When we say Notice that these laws have no specific notion of The "gap" is that you know about Issues like this crop up in other places as well. For example consider #3079. The original implementation of
Sorry, I missed this. Do you have a reference issue about |
No, Im sorry.. 😏 Loose talk on my part, I was referring to the |
No problem, thanks for clarifying :) So I can answer that question now too, and it's essentially the same situation as Specifically, when you have Indeed, if you only ever use The tl;dr here is that laws can only make guarantees about the behavior of implementation of the typeclass, not "special" behaviors of the monad. So when you use the monad's "special" features, their behavior is not specified by the Cats Effect laws. This is why the behavior can be both lawful but "surprising". |
Btw, I'm a broken record again, once again the solution is to use Cats MTL. Instead of https://typelevel.org/cats-mtl/mtl-classes/handle.html The |
Yes, think about this: the platform provides you with several primitives: 1) calling subroutines, 2) creating data, 3) mutating references, 4) creating system threads, 5) introducing synchronisation. These capabilities cannot be expressed in terms of each other, otherwise they won't be primitives.
With this, you're stating that basically you only want to use primitives 1) and 2) , and so by definition you won't be able to deal with primitives 3, 4 & 5, and you'd have to necessarily resort to a version of concurrency that uses The ingenious trick or, depending on your perspective, the ugly hack that
Now, let me challenge these statements a little bit.
It's actually hard to find a definition of purity that justifies this statement. Both
Well, in a way the very existence of this issue kinda disproves it but let's entertain the statement that all things being equal things like
Now, this might be true if you compare: def myFun(a: Int): WriterT[IO, Bar, String] with case class Foo(logs: Ref[IO, Bar]) {
def myFun(a: Int): IO[String]
} (although again, the first is fundamentally not as powerful as the second), but I don't think it's true for something like: trait Logs[F[_]] {
def log(b: Bar): F[Unit]
}
def myFun[F[_]: Logs : Concurrent](a: Int): F[String] which you can implement over
I'll give you that doing:
it's slightly easier than: def myFun[F[_]: Logs : Concurrent](a: Int): F[String] = ...
IO.ref(emptyBar).flatMap { state =>
implicit val myLogs: Logs[IO] = Logs.fromRef(state)
myFun(3).mproduct(state.get).flatMap { case (logs, value) =>
assertions(logs) >> assertions(value) but is it that much simpler? Especially when you consider that you can extend
I have to agree with you on this, however. @armanbilge has explained well why they can pass laws and still behave unexpectedly, but the end result of user confusion is all the same |
Yes, just for the record I believe we should deprecate these instances. I expressed a similar opinion in typelevel/fs2#3246 (comment). |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
I'm seeing FS2's parEvalMap dropping logs emitted by
WriterT[IO..
, while the sequential cousinevalMap
retains them, as does a "desugared" Writer usingparEvalMap
.Since parEvalMap is written in terms of
Concurrent
this hints at a potential problem in the WriterT instance, although I was not able to spot a problem from a visual inspection.Cats Effect 3.5.1 and FS2 3.7.0
The text was updated successfully, but these errors were encountered: