Interesting take! But I think we don’t really need to copy F# verbatim here. F# does certain things the way it does, because it lacks other features that Scala possesses even today. Needing to have an explicit “builder” object is one such example. In Scala, we have implicit parameters, extension methods and higher-kinded types. We should do fine with only those. What we would need, is a new do
construct.
Let me illustrate it with this short snippet:
def fetchAndDownload(url: Url): IO[Data] =
do { // marks the start of the computation expression. alternatively
val urlStripped = strip(url) // usual variable binding; it's nice you can do that even as the first thing
val! data = downloadData(urlStripped) // `val!` desugars to `.flatMap`
val processedData = processData(data) // another usual variable binding
if!(callVerificationService(processedData).map(_.isError)) { // `if!` with then branch `IO[Unit]` and without else branch desugars to calling `.whenA`
throw! CustomError("wrong data") // `throw!` desugars to calling `.raiseError`
}
use! monitor = createMonitor(processedData) // `use!` desugars to calling `.use`
do! notifyMonitor(monitor) // `do!` on `IO[Unit]` desugars to calling `.flatMap(() => ...)`
processedData.pure // `IO[Data]` is expcted here, so we produce it by calling the `.pure` extension method
}
The precise names of the extension methods this desugars to is not terribly important, I just used the names as they are in cats toady. But Scala authors could choose different names, and then, I’m sure, cats, zio & co. would accommodate.
What is important, is that the user-visible syntax of the “monadic” operations is intentionally similar to that of their imperative counterparts, so val
/val!
, if
/if!
, throw
/throw!
and so on…