I spent a bit of time thinking about how straightforward it would be to use the approach that is used by F# computation expressions and to me it seems kind of promising. Writing the underlying builder class looks like it could be pretty straightforward, and the translation of the syntax within a block doesn’t have to be so complicated, see e.g.:
My thoughts are that we get a few advantages from copying the F# approach:
- Most of the new Scala developers I have worked with are totally mystified by the idea that
for
can be overloaded to work on non-collection type things. The builder syntax makes it a bit more clear that something magical is happening and that it’s not just the normalfor
loop that they’re used to. - Doesn’t really use any new keywords as we can reuse old ones inside the block (e.g.
for
,while
,do
) - Individual method signatures can be as complicated and constrained as required by the underlying libraries
- Builders can be as polymorphic or as specific as we want, e.g. we can still write an expression builder against
F
if we want
and a few disadvantages:
- You would really need to specify the F you are interested explicitly in if there are any non-trivial constraints involved rather than letting type inference do the work
- The fact it’s a syntactic transformation limits optimization opportunities a bit as you can’t do dependency analysis of the expressions, but this is probably OK because ordering is significant here anyway
- We really can’t write something as generic as the original
for
comprehension using this approach - the builder class has to typecheck - Is reusing syntax like
for
a good idea? Then expressions that use the results oftraverse
look pretty weird due to multiple<-
, i.e.
async[Task] {
deploymentIds <- for (name <- verticleNames) {
Vertx.deployVerticleL(name)
}
do StdIn.readLine("Press enter to shut down")
for (deploymentId <- deploymentIds) {
Vertx.undeployVerticleL(deploymentId)
}
}
Some interesting questions:
- How to let the compiler know that this builder class triggers the desugaring? Extending a magic trait and other similar things seem kind of gross.
- Could this be implemented by making the builder factory method a Scala 3 macro that accepts the block of code to desugar? Would that limit the syntax that could be used within the block to things that superficially typecheck before the macro expansion or would they just have to parse?