Pre SIP: `for` with Control Flow (an alternative to Scala Async, Scala Continuations and Scala Virtualized)

Since @julienrf has mentioned it I’m really interested in all of your thoughts about a syntax inspired by the F# computation expression.

Please excuse my idiosyncratic notation!

For some computation builder bldr:

// Builder expression
// 
// Returns a custom class implemented by the user.
// 
// It contains declarations of the DSL operations supported by this
// computation expression.
// 
// As a syntactic transformation only, unsupported syntax would trigger a compile error
// as the current for comprehension does now when it is used with types that don't support
// the required operations
//
// def bldr[F[_]]: BuilderType[F]
//
//
desugar('{ bldr[F] { expr } })
  ==>
    val $ = bldr[F] // $ would be some fresh name
    desugar('expr)

// Compound expressions in a builder
desugar('{ cexpr })
  ==>
    desugar('cexpr)

// Bare expressions in a builder
desugar('{ a })
  ==>
    a

// Unused enumerators in a builder
desugar('{ b <- a })
    desugar('a)

// Composing expressions
//
// def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
//
// bare expressions:
//
desugar('{ a; expr })
  ==>
    $.flatMap(desugar('a))(_ => desugar('expr))

// with enumerators
desugar('{ b <- a; expr })
  ==>
    $.flatMap((desugar('a))(b => desugar('expr))

// Yielding values
//
// def unit[A](a: A): F[A]
//
desugar('{ return a })
  ==>
    $.unit(a)

// Possible optimizations
desugar('{ return a; b <- a })
  ==>
    $.flatMap($.unit(a))(b => f(b))
  ==>
    f(a) // via monad left identity

desugar('{ b <- a; return b })
  ==>
    $.flatMap(desugar('a))(b => $.unit(b))
  ==>
    a // via monad right identity

// def map[A, B](fa: F[A])(f: A => B): F[A]
//
desugar('{ b <- a; return f(b) })
  ==>
    $.flatMap(desugar('a))(b => $.unit(f(b)))
  ==>
    $.map(desugar('a))(f) // via map/flatMap coherence law
    
// For loops
//
// def traverse[G[_], A, B](ga: G[A])(f: A => F[G]): F[G[B]]
//
desugar('{ for (b <- a) { c } })
  ==>
    $.traverse(a)(b => desugar('c))
    
// Conditionals
//
// def ifThenElse[A](cond: F[Boolean])(thenExpr: F[A])(elseExpr: F[A]): F[A]
// 
// def zero[A]: F[A]
//
desugar('{ if (a) then b })
  ==>
    $.ifThenElse(desugar('a))(desugar('b))($.zero)

desugar('{ if (a) then b else c })
  ==>
    $.ifThenElse(desugar('a))(desugar('b))(desugar('c))

// While loops
// 
// def while(cond: F[Boolean])(body: F[Unit])
//
desugar('{ while (a) { b } })
  ==>
    $.whileLoop(desugar('a))(desugar('b))

// Lifting plain Scala code into the builder type
// 
// def delay[A](a: => A): F[A]
//
desugar('{ do a })
  ==>
    $.delay(a)

Obviously this is not really close enough to a formal specification of such a desugaring but I hope this helps illustrate the idea.

You can see what the experience of defining a new builder type would be like in the following code sample, along with some example expansions of the syntax.

I expanded a bit on the advantages and disadvantages of this in the other thread, but there are some unanswered questions about porting such a design to Scala:

  • How could the compiler know that this builder class triggers the desugaring if there are no enumerators present? Extending a magic trait and other similar things seem kind of gross and also don’t really fit into a syntactic expansion of the desugaring.
  • 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? The former is probably too limiting.
  • Is reusing <- a bad idea? What else could we use? It collides with the for syntax, especially when using the results of a for expression, i.e. deploymentIds <- for (vertName <- vertNames) { Vertx.deployVerticleL(vertName) }