4 ms·
Yeah this was the same discussion that happened when monadless was released in the Scala community. I personally don't see much of a difference between reorderi
by dwohnitmok 5y ago
Yeah this was the same discussion that happened when monadless was released in the Scala community. I personally don't see much of a difference between reordering `<-` lines and reordering !-ed expressions, especially in a strict language (such as Kind). The same arguments for how you rearranging different !-ed expressions can result in different behavior whereas non-!-ed expressions can be reordered with no change ring exactly analogously to me for `<-` statements vs the usual `=` in a let expression.
The fact that
val x = something
val y = somethingElse
doThis(x, y)
allows for interchanging x and y if `something` and `somethingElse` are pure but
for {
x <- something
y <- somethingElse
} yields doThis(x, y)
doesn't allow for interchanging `x` and `y` feels approximately just as confusing (or not-confusing) as
x = something
y = somethingElse
doThis x y
having interchangeable `x`s and `y`s but
MyMonad {
x = !something
y = !somethingElse
doThis x y
}
not having interchangeable `x`s and `y`s.
I suppose what you're really getting at is what happens when you combine it all into a single line a la
MyMonad { doThis !something !somethingElse }
but I still don't find it confusing for a strict language (and besides legions of developers have had a chance to familiarize themselves with this via async-await). The rules in either case are the same. If you move around `->`s behavior might change. Likewise if you move around `!`s behavior might change.
In a lazy language such as Haskell I agree that it can be rather weird to have to think about how inserting a pair of brackets and some `!`s around suddenly causes sequencing that is different from how a normal statement is evaluated (although the bangs are quite suggestive, especially given their lineage in Haskell, indeed given a `Lazy` monad these bangs just become the same thing as bangs in Haskell, so another way you can view this is as a generalization of Haskell bang patterns). But Kind is a strict language AFAICT and in a strict language evaluation from left to right and top to bottom is the norm. And that left-to-right, top-to-bottom order immediately tells you what the code paths are.
Even in a lazy context where the order of evaluation might be a bit more confusing, this async-await style of writing code is still valuable for delineating which code blocks are side-effectful are and which aren't. Checked exceptions/checked effects are still very valuable! It's valuable enough that e.g. the Haskell community is perfectly willing to compromise on knowing what all the code paths are with stuff like ApplicativeDo.
Having spent a considerable time with cats-effects in Scala and a little time with Kotlin's Arrow Fx there are times I still prefer Arrow Fx despite the general clunkiness of Kotlin's Arrow ecosystem over cats-effect precisely because I just need to stick in a `suspend` instead of having to contort perfectly fine code into a for-comprehension when I introduce some effects simply because I didn't write my pure code with the Identity monad the first time around.
- lmm 5y ago> but I still don't find it confusing for a strict language (and besides legions of developers have had a chance to familiarize themselves with this via async-await). The rules in either case are the same. If you move around `->`s behavior might change. Likewise if you move around `!`s behavior might change. The <-s are always in the same place on the line and always ordered top-to-bottom, whereas it's not at all clear what ordering applies to the !s - it's generally "evaluation order", but the whole point of using monads is to get away from relying on evaluation order! When it gets confusing is when you're doing something that has a nontrivial evaluation order in the expressions, e.g. MyMonad { something sortBy { (x, y) => someComparison(x, y, !z) } } Even left-to-right evaluation of function arguments isn't always obvious - for some functions it would be more natural to evaluate in a different order, and not all strict languages evaluate left-to-right (I believe in C/C++ the order is undefined). I agree that it's not so different from async/await or ApplicativeDo, but I find both those things confusing, and I hate dealing with arrow FX precisely because I can't follow what the code is doing except by thinking through what all the evaluation is doing operationally.
- dwohnitmok 5y ago> whereas it's not at all clear what ordering applies to the !s - it's generally "evaluation order" But the evaluation rule for ordering of effects is fairly simple. Left-to-right (taking into account paren nesting) and then top-to-bottom. It's the exact same as the `<-`s, just with an additional left-to-right dimension. I brought up "evaluation order" in languages because I wanted to drive home that this left-to-right, top-to-bottom order is natural for developers (and C/C++ is a well-known outlier among imperative languages in this regard with a full stable of unspecified and undefined things, but Java, Python, Javascript, Rust, etc. all evaluate in that order). But it's not actually essential that any of the non-monadic code obey that evaluation order. You don't actually rely on evaluation order anymore than the sequencing provided by monads. All the expressions in the middle could be lazily or eagerly evaluated, it doesn't matter. You bring up a good point with closures in the form of anonymous functions/objects, but that is also the only place that I would argue it's confusing for a developer coming from an imperative background (some who might expect this to be an exception to the left-to-right, top-to-bottom rule as it is e.g. with async-await in Javascript where a closure is not created and the Promise is regenerated where it is created), but you could just disallow it there or require additional annotation of the anonymous function, as e.g. happens in most languages such as Rust and JS. Even there the left-right, top-bottom rule could apply, it might just confuse some developers, who are used to it having an exception in anonymous functions. Everywhere else not only does left-right, top-bottom give a concise and evaluation-independent (up to the sequencing constraints imposed by the monad) description of how the effects will be executed (which can be made universally true if you're willing to tolerate it for closures or you can just disallow `!`s in anonymous function bodies), this ordering also agrees with what developers coming from other languages would expect. Put another way, I don't think most developers will be confused by the execution order of effects of `!` that doesn't have anonymous functions. I think you could ask any developer, even those unfamiliar with FP, what order they think the effects preceded by a `!` will evaluate in the following expression and the overwhelming majority of them would get it right. MyMonad { x = !effectfulOp0 someFunction x !(effectfulOp1 x !effectfulOp2) !effectfulOp3 } Again you don't need to follow what the evaluation is doing operationally. You just need to do a syntactic scan. No matter what evaluation strategy is used, the order of effects will always be `effectfulOp0`, `effectfulOp2`, `effectfulOp1`, and then `effectfulOp3`. There is no operational reasoning here at all. It's purely syntactic and local. This is true even for monads that seem to have a weird order of effects such as the reverse state monad, you just have to be very specific about what the effect actually is (the addition of thunks in the case of the reverse state monad), which is the same case as with do-notation, which is why lazy IO is so confusing, even though technically the monadic sequencing of effects is still being followed. Indeed this distinction between operational and denotational reasoning has nothing to do with syntax (as it shouldn't since those are semantic questions), but rather your choice of monad. As Conal Elliott rightfully points out monadic IO is intrinsically a dismal failure if you want denotational semantics instead of operational semantics to begin with, even were you to write out everything with bind syntax. The problem is not with the syntax but with the monad itself (Conal would probably prefer the word monad here to have scare quotes around it when referring to monadic IO). At the end of the day I view the differences between async-await syntax and do-notation as a difference in degree rather than kind (and indeed this is usually how I explain do-notation to developers familiar with async-await), whereas I think there's a quantum jump from normal bind/flatMap nesting and do-notation. However, I view the syntactic affordance given by async-await syntax to be a much larger win, especially if your language already begins with a block-inspired syntax with variable declaration statements rather than a lambda calculus-inspired syntax with let expressions.