-
Notifications
You must be signed in to change notification settings - Fork 58
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
AsyncEither documentation #19
Comments
There's not a lot of documentation on any of the monads besides IO and IOx. That sort of reflects the bias of this library, which is that it wants to funnel you into using IO/IOx, where the other monads serve as side dishes rather than the main course.
The differences are more implementation that conceptual, but...
I understand that. I endeavored to provide a better, more ergonomic approach to that via IO's IOW, that's basically like a better form of how the
I appreciate that, and typically agree. But Monio is specifically designed to have IO (and IOx) be attractive as "uber" monads that compose all of the functionalities you need for you, instead of requiring you to do the compositions yourself. By offloading that composition to Monio, you get a ton of complexity dealt with for you, and that also means more opportunities for Monio to performance-optimize where your code might be the lesser-optimized equivalent by default. I think it's simpler that with All that is to say, I encourage you to think about using IO, even if just But there may very well be limited cases where you're doing some success/failure style asynchrony that is not particularly side-effect'y -- though I argue asynchrony is inherently a side-effect -- in which case I just don't think it should be your generalized tool for asynchrony, as I think IO is likely the better (certainly more capable and optimized) option in most cases. |
@getify Thanks so much for the thorough reply! I'll try out both approaches in my project and see what ends up feeling better for my coding style.
One huge pain point regarding Promises is the mixing of exceptions and purposeful rejections. I'm mainly looking for a tool to separate those concepts out which it what attracted me to Futures in the first place (speaking specifically of the FlutureJS library). Is there a particular strategy for handling that with IO? I'm also not sure how I feel about |
That's exactly what it is, and it's by design (even in Haskell!). It's a convenience and a compromise. In particular, it is the motivating feature behind this whole library, because I feel it offers the bridge between the declarative (and often heavily chained/composed style) of FP and the more familiar (and more imperative) JS style. I'm trying to bridge that gap and get more JS developers into using the IO monad (and other monads, too), so
Beyond simply being an attractive bridge that offers the imperative compromise that's still encapsulated inside the protections and benefits of an IO monad, there are a number of challenges that any chain-style API suffers -- that includes jQuery, JS Promises, RxJS, and virtually any other tool you can name with the fluent API style. One such disadvantage, in particular: when you need to propagate more than one value from step to step in a chain, you don't have any surrounding scope in which to do so. const getFirstName => () => IO(() => document.getElementById("first-name").value);
const getLastName => () => IO(() => document.getElementById("last-name").value);
const renderFullName => (first,last) => IO(() => document.getElementById("full-name").innerText = `${last}, ${first}`);
getFirstName()
.chain(firstName => getLastName())
.chain(lastName => (
renderFullName( /* OOPS, firstName not in scope! */ , lastName )
))
.run(); So, you either have to nest chains to create a shared scope... getFirstName()
.chain(firstName => (
// ugh, nested chaining, ANTI-PATTERN!
getLastName().chain(lastName => (
renderFullName(firstName,lastName)
))
))
.run(); ... or, worse, you have to artificially pack multiple values into some container (like an array or object), pass that from one step to the next, then unpack it. This sort of effort muddies up both the return values and the parameter definitions of all involved functions. And with IO, in particular, since it's lazy, that's even harder to do. To avoid chaining, you may have jumped through such hoops with Promises -- and if you have, you know how much that sucks! -- but to do so in IO you may go about it like this: getFirstName().map(v => [ v ])
.concat( getLastName().map(v => [ v ]) )
.chain( ([ firstName, lastName ]) => renderFullName(firstName,lastName) )
.run(); I mean, I think that is a cool usage of Concatable ( IO.do(function*(){
var firstName = yield getFirstName();
var lastName = yield getLastName();
yield renderFullName(firstName,lastName);
})
.run(); IMO, that's just resoundingly better (clearer, more maintainable, more familiar) code. Opinions differ of course. There are other imperative constructs which can be useful (and/or more performant) in certain scenarios, which having a do-block lets you perform. For example, sometimes you just want an |
I know some people are bothered by the exception space being mixed between intentional and unintentional exceptions. I am not bothered by it, and in fact I prefer it. Whether I plan an exception case, or it happens by accident, I still treat it as my responsibility to gracefully handle it. I find that easier to do when the paths are consolidated than when I have multiple exception paths to juggle. I designed Monio from that perspective, but that's just my take. I'd be curious how you saw Futures (and specifically Fluture) as addressing that? The implementations I examined for Future didn't seem to have separate channels for intentional vs unintentional exceptions, but perhaps I missed some details. Putting on my FP'er hat for a moment, I think one reason Either is liked is because it makes defining intentional exceptions pretty explicit (and the handling of them). My guess would be, FP adherents would answer your question by saying a library should not marshal unintentional (JS run-time) exceptions into Either:Left's -- therefore you'd know if an exception was intentional or not if it was wrapped in an Either:Left or not. If you used I think there's a lot of merit to IO even in that "mode". But I just happen to personally think there's even more capability to take advantage of if you consolidate the exception paths, and that's what I prefer. |
Thank you again for the responses! I appreciate the example illustrating the ergonomics of
The author of the Fluture has this as one of the main selling points for the library. He can explain it better than I can so here's a link to an article about it. It does boil down to preference but I've often found myself frustrated by the mixing of the two concepts. https://dev.to/avaq/fluture-a-functional-alternative-to-promises-21b#branching-and-error-handling |
Thanks for the link... interesting stuff. I think the "railway oriented" concept is entirely compatible with both how For the IO type(s), the re-combine point is a bit more manual: after the What's interesting/different there is this part mentioned toward the end of the article:
That is a philosophical difference between Fluture and Monio, and I'm happy to own that. This mindset comes from more statically typed (and compiled) languages where run-time errors are almost impossible, and even if possible are exceedingly rare. I don't think JS developers (even TS developers) would characterize JS development that way. However, practically speaking, in JS, I'm not sure quite what this in particular means... or rather, whether it represents a difference from Monio or not:
But they're still mixed in with the exceptions you might have intentionally done like a If you think about it that way, What's not equivalent is that If you choose to use a type like What it comes down to, I think, is:
I may be missing other details/nuances, but I think broadly that describes the choices I'm hearing from you, with respect to using Monio. I can't make those choices for you, but I hope this conversation has been helpful to pick out the relevant points of decision. :) |
Hi!
I was curious if there is documentation for AsyncEither. I see it was recommended to use the IO monad* but I prefer the error handling of AsyncEither (and I like having a more specific tool). Also from the same discussion: it's mentioned that "The best way to think about AsyncEither is that it's like a Future monad", what exactly is the difference between an AsyncEither and a Future?
Thanks for your time!
The text was updated successfully, but these errors were encountered: