WriterT[IO.. losing logs in parEvalMap hints at problem with Concurrent instance for WriterT ? #3765
Replies: 15 comments 12 replies
-
See some related discussion in typelevel/fs2#3246 (comment). tl;dr don't use monad transformer datatypes. You can replicate |
Beta Was this translation helpful? Give feedback.
-
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.. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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..
|
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
No, Im sorry.. 😏 Loose talk on my part, I was referring to the |
Beta Was this translation helpful? Give feedback.
-
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". |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
Yes, just for the record I believe we should deprecate these instances. I expressed a similar opinion in typelevel/fs2#3246 (comment). |
Beta Was this translation helpful? Give feedback.
-
I converted this from an issue into a discussion because it feels more appropriate for this space. To be clear, I think this is an absolutely fascinating topic and I'm grateful you brought it up, @benhutchison. I have some long-form thoughts that I don't have time to write at present, but I'll try to get to it. This is a very tricky space, and I generally agree with your dissatisfaction with the current state, but the "where to go from here" is a bit less clear to me. |
Beta Was this translation helpful? Give feedback.
-
This issue caused typelevel/cats-mtl#516 |
Beta Was this translation helpful? Give feedback.
-
An interesting epilog: As I mentioned earlier, I implemented my log API in terms of 2 impls of For simple logging events, both impls worked well. However, I also have a And when I test this using the For me, this is the final nail in coffin for my hopes for Writer logging. I give up. IME it isn't usable in practice, even for unit tests. |
Beta Was this translation helpful? Give feedback.
-
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
Beta Was this translation helpful? Give feedback.
All reactions