Hey, I am sure this already has been brought up, but I think for
is trying to serve too many masters. There is collection heavy code with filtering and such and then there is monadic code. And while there exist a big overlap between the two, maybe it is still worthwile considering the two in isolation.
I, for myself, am writing a lot of monadic code recently and noticed that for
is really getting into my way in doing so. Here is a snippet that illustrates some of the pain-points:
for {
cur <- pos.value
cacheKey = (key, cur)
cache <- resultCache.value
res <- if (cache isDefinedAt cacheKey) (for {
(results: A, newPos) <- pure(cache(cacheKey))
_ <- pos.update(newPos)
} yield results) else { ... }
} yield res
With some form of do
-notation this could be written as
do {
val cur <- pos.value
val cacheKey = (key, cur)
val cache <- resultCache.value
if (cache isDefinedAt cacheKey) do {
val (results: A, newPos) = cache(cacheKey)
pos.update(newPos)
pure(results)
} else { ... }
}
I am purposefully using do
here to clearly distinguish from the existing for
.
Note that val ID <- EXPR
translates similar to ID <- EXPR
in for
and val ID = EXPR
translates similar to ID = EXPR
in for
. To be more compositional, the last value in the do
-block has to be monadic, hence the pure(results)
in the translation to explicitly lift results
. At the same time
[[EXPR1; EXPR2]]
translates to val _ <- [[EXPR1]]; [[EXPR2]]
.
That is, sequencing is monadic sequencing. This makes “builtin side effects” more cumbersome to use via val _ = println(...)
. However, I feel this overhead is appropriate since do
switches citizenship between builtin side effects and managed side effects.
I am aware of monadless and other approaches to ease monadic programming, but I couldn’t find a dotty SIP.
@odersky You mentioned, you don’t want to go back to val
s. Would that also hold if one separates for
and an alternative do
-notation? I am purposefully using val
s here to highlight that do
blocks “just” overload sequential composition.
edit: Thinking about it a bit more: If we ignore overloading of ;
for a moment – only having val f <- EXPR
to translate to flatMap
which scopes over the rest of the current block would already go pretty far.