| > Do notation is a syntactic solution But it's coupled to a monad interface in the language (or equivalent concept e.g. a typeclass in Haskell). It's not just a superficial similarity of syntax, you can write reusable functions that work for any monad, and the monad abstraction is very useful even if you prefer to write https://fsharpforfunandprofit.com/rop/ - style code that doesn't make use of do notation at all. The syntax looks the same because the values its working on implement a common interface - do notation itself is a very lightweight sugar that translates directly into interface methods. > The initial version does explicitly handle errors. The async/await based version also handles errors (any exceptions thrown by intermediate functions will be thrown by whoever is trying to use the results). What will the do-notation continuation version do with any errors returned by the intermediate functions? Neither the async/await nor the promise-based version includes any explicit error handling. So you're applying a real double standard; how is it any more or less clear what the monadic version will do versus what those versions will do? > Not really - it is usually very clear how exceptions integrate with other control flow features such as continuations or, much more easily, promises or async/await Having had to deal with some exceptions in sequence-continuation and async/await-based Kotlin code only last week, I have to disagree. Often there is more than one possible way to combine two effects, but if those effects are language features then the language designer will have picked one and you have to hope it's the behaviour you wanted. (E.g. if a parallel loop throws an exception, does this fail the other cases or just that one result? If an async function throws an exception before the first await, does the exception happen immediately or get suspended? There's no right answer, there are use cases where each behaviour is what you want). > I completely disagree with the idea that language keywords make refactoring difficult in any way, or that a language with less syntax is always easier to work with. It's not the syntax that's the issue, it's the semantics. In my experience the majority of production regressions caused by code changes (most production bugs tend to be caused by config changes, but that's a separate rant) come down to a particular category of language feature: things that can't be understood as plain old functions or values (to use the FP jargon, things that violate referential transparency). E.g. hoisting a value out of a loop changes the behaviour because the method that computes the constant was actually throwing an exception. E.g. inlining a method means a transaction decorator isn't applied. E.g. pulling out a common expression into its own function means it gets executed on a different thread. So replacing a language feature with a function that I can read the source code of (and, even more importantly, can be confident will follow the normal rules for functions in the language) is the best way I've found to eliminate whole categories of bugs at a stroke. > Even the designers of Haskell disagree with this, as they have included both do-notation and list comprehensions, instead of letting programmers simply use bind and return. What I'm arguing is that dedicated language constructs have a cost - of course that cost is sometimes outweighed by the benefits. But if you can replace five different special cases with a general case that covers all of them (and isn't five times more complicated to understand), that's a huge win. |