-
Notifications
You must be signed in to change notification settings - Fork 95
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
"Break early" from monad CE #135
Comments
If you replace it with This as weird as it seems, it's a bit standard in Computation Expressions, actually last week someone asked at work practically the same question over async workflows:
It launch, unless you use |
Oh, my mistake then about Also, there's other weird behavior. Consider this: use enum = ([0] |> Seq.ofList).GetEnumerator()
let result = monad {
while enum.MoveNext () do
ignore enum.Current
} This fails on |
I don't have enough context to compile your code (monad is not inferred). |
Does the monad type matter? Try |
No it shouldn't matter. What does matter here is the type of CE (lazy/strict). If you use the strict one ( The offending commit is this one: 4d07943#diff-8ed049da6bf12bafde3fe125f70862fb I was between two worlds here. But I wanted to be smarter and found a way to "autosense" the kind of computation, it works in most cases, but in some others (like this one) it will throw a runtime error. I'm not 100% convinced if it was the best decision. |
I've got no idea about this auto-sensing, but do you agree that the |
It's not a bug, in the sense that I can understand and explain why does it happens. And it will fail immediately and systematically in the first iteration, still it's a runtime error which is not that great. Moreover, it wasn't unexpected to me, when you first posted it I was 99% sure that was the reason, just wanted to reproduce it to make sure. We can discuss and eventually decide to revert the auto-sense and convert that scenario to a compile-time error. I think the problem is that understanding Computation Expressions is not straight-forward, because you need at some point to understand how the code is desugared and how it's executed. My guess is that understanding 4 main CE's models is worth the effort in place of understanding the model of every single custom CE in the wild. On top of that, we can certainly add more documentation to those 4 generic models, they definitely deserve it. Some additional tests are very beneficial and so is this discussion, I'm happy that you are digging into this as we can still decide what's the best approach, and eventually rectify some decisions. |
I completely agree that thorough documentation on the four generic CEs would be prudent. :) As you say, CEs can and do bite, and understanding how they are desugared and run can be far from trivial even for experienced F# devs. I'll certainly stay away from these four CEs until they are extensively documented; I won't risk runtime exceptions for reasons I can't explain on my own and that aren't documented just for added convenience. |
|
@cmeeren Just in case you are interested, I can give you more details of what's the reason. In short, all monads (or CEs) can be made strict, but not all monads are lazy by nature. This contradicts a bit the design decision of using the lazy one as default, which was motivated by the fact that the lazy one was more interesting. As a result, one nice thing is that in most cases you don't need to explicitly mark your CE's as strict, in most case they will work, unless they use an enumerator (apart from the enumerator, there were other scenarios where it would fail but the "auto-sense" solved those ones). So, if a CE makes use of an enumerator it has to either:
If none of these 2 conditions are met, you'll get a run-time error in the very first iteration. Again, this run-time error is expected in the sense that you are forcing a delayed construct to iterate but the delay function is not lazy, so that won't work since it's not really delaying anything at all. That's for instance the reason why, ReaderT and StateT monad transformers have a Now, if you don't agree with this design, I would be happy to discuss what the alternatives are. |
Thank you for the explanation! I really appreciate it. I am very fuzzy on the whole lazy vs. strict thing; I don't really understand exactly which parts of CEs are lazy or not, nor exactly how the Delay function impacts this. When I created Cvdm.ErrorHandling, I very much relied upon the unit tests to verify the behavior I expected, and hammered at the CE builder implementation until the tests were green. Please take a look at the unit tests for the As for what you mention about enumerators: Are you saying that in the very specific case of having an enumerator where you call Also, I'm wondering how the |
See FSharpPlus/src/FSharpPlus/Builders.fs Lines 55 to 71 in fcb507b
The main difference is that a lazy builder needs to handle special cases to deal with delayed computation. As you can see in the code, it requires a custom Delay, TryWith, TryFinally and Using methods, whereas the strict one can use trivial definitions for those ones. I'll have a look at how it relates to your implementation of the Result CE, but note that Result is in principle a strict CE, which explains why (together with your curiosity) you are finding so many issues here. Your unit tests approach is great, but here it's more complicated, because we have to test the same but for many scenarios, it would be nice to add more tests here. I'll have a look at your implementation. Regarding the enumerators, that was my thought at the beginning, but this other issue you raised #137 makes me think that in fact all custom methods I mentioned above are somehow affected. It behaves differently because as you can see the while implementation of a strict and a lazy one is different, and all these methods together should be aligned in the same evaluation strategy. Regarding your question about async, it is my understanding that it should behave exactly the same, as the standard F# core async workflow is both lazy and fx (as opposed to monad plus), which are currently the defaults for simply |
What does the different names mean?
? |
@gusty Thanks. I tried the strict |
Yes, the different between an fx and a monadplus monad is that the combine function in the former is coded in such a way that allows to execute side-effects inside the CE, whereas the one in the monad-plus allows to combine many values (a monad-plus can return many times). So for instance, a first return in an fx would be an early termination, but not in a monad-plus. So, defining All this design was heavily inspired in the Computation Expressions Zoo paper. |
Thanks. Do you know if it would be possible to define something akin to |
What happens when you do:
? |
You have also have similar constructs in ExtCore (as in your library). I did a trivial, but huge pull request. Some of the tests can be ported to f#+. |
Doesn't work. If I try to constrain the types as much as possible, e.g. let result : Result<string, string> =
(monad { return x }: Async<Result<string, string>>)
|> Async.RunSynchronously then I get an error on |
Thanks. I will experiment some more during the weekend. |
In order to get a similar behavior to the asyncResult you need to use Monad Transformers. The advantage of Monad Transformers is that you can combine the transformer with any monad, otherwise you would end up writing a lot of boilerplate, and any new monad will increase that boilerplate in an exponential way ! Monad transformers are also monads, but they have a different type, that's why your snippet doesn't work as you expect: the compiler is not expecting an asyncResult monad by the sole fact that the result contains a result inside the Async. If that was the case, it would be trying to be too smart and it will apply the asyncResult in cases whare just the async was intended. You ommited the definition of x, but I guess it's a plain string, so you're failing code is probably something like:
You can get it working, of course by setting
|
What is |
Yes, there are no docs here that explain the concept from scratch, as there are very good Haskell tutorials in place, so the docs for now assume you are familiar with them. Actually the same can be said about monads alone and so on. It would be nice to have some explanation, but if we doc everything, from scratch, with good sample, we will end up creating an F# for fun and profit site on our docs. Which is great ! But a lot of effort. |
To answer your specific question:
This way the generic CE get the right overload which combines both monads (or more), once your CE stuff is done, you can get out of the wrapper with Not sure if it's clear, otherwise let me know. |
You have the following example: https://github.com/fsprojects/FSharpPlus/blob/master/src/FSharpPlus/Samples/MonadTrans.fsx |
Thanks, I'll have a look at the samples again with new eyes. In any case, I'd recommend linking from the documentation to those
you mention. |
Yes, we need at the very least provide links. |
@cmeeren some stuff on monad transformers in F# in SO https://stackoverflow.com/questions/tagged/f%23+monad-transformers |
Can we close this in favor of #140 ? |
Sure! |
@gusty In a previous comment, you said:
As I understand it, |
I'not sure I understand your question. What I meant is that CEs not necessarily end at the |
I think it worth clarifying I'm not saying the opposite (actually here's the confusion) that all non monad plus CEs ends at the return point. We can say that monadically talking they will end there, but they can have side effects as in that case. Now using an |
What I meant is this: You explicitly say in this comment that a first return in an fx monad is an early termination. I interpret this as meaning that no code after the first return will be executed. However, in this comment, you say that in this code: let threatLevel = 1
async {
if threatLevel < 5 then
return ()
System.Console.WriteLine("Launch missiles")
} |> Async.RunSynchronously then the side-effect will indeed be executed, even if it's after the first return. Furthermore, my understanding is that Thus, we have following incompatible statements:
Could you clarify where the error is? I'm guessing it's point 2. |
Sure, in short: 1-2 It's an early termination monadically speaking, I mean, no value will be returned afterwards, but side effects might still occur. 3-4 Async is definitely an fx monad |
@cmeeren FYI when #364 gets merged and released, it will make code like this to display a warning explaining what would happen at runtime and how to solve it. Also, if you read the PR comment, you'll have a detailed description of why this happens. So, after 2 years you should be on the safe side now ;) |
I have a
result
CE in Cvdm.ErrorHandling for monadic error handling. For the example below,sideEffect1
andsideEffect2
are never invoked, sincedo!
binds an error (I think alsoDelay
is involved, but I don't have a clear grasp of CE builders).However, if I replace
result
withmonad
(ormonad.fx
or others, I've tried them all), the side effects are invoked.Is it possible to get my desired behavior using a generic CE from FSharpPlus?
The text was updated successfully, but these errors were encountered: