Scala 3 workarounds for dropped features?

While reviewing the 2020-03-11 SIP meeting notes, I noticed a variety of proposed changes that, taken together, will absolutely clobber my existing code in the absence of a workaround. They’ve been mentioned before, but I only just realized now some of the interactions.

Of course, some of these probably have workarounds, or are less bad than they seem (because there are flags to change the behavior, at least temporarily).

Is there, or can there be, a place where workarounds are collected for all dropped features? I think this may be a relatively high priority when deciding upon whether changes should really go ahead, because without them (and without the list of dropped features) it’s hard for people to judge how much burden it would be to rewrite their code, and thus also make it hard for them to give accurate feedback.

I’ll give one example, about the pair of features that would potentially impact me the most–macros are changing and nonlocal returns are going away.

I use a macro to create a relatively straightforward rewrite of foo.? to

foo match {
  case Right(r) => r
  case Left(err) => return Left(err)
}

which mirrors Rust’s ? operator, except even better because it works inside collections operations etc. due to non-local returns.

The original rewrite seems doable with the splice-style macros in dotty. But if you don’t have nonlocal returns built in, the rewrite is considerably trickier–you need to take the entire enclosing method body, wrap it in a nonlocal-return-block, but only if it’s not already in one, and then do the local rewrite. Can dotty macros do that?! It seems doubtful–you have to explore outwards from the use site to the surrounding context. And this is absolutely ubiquitous in my code–something like 1/6th of my methods use this (in the largest codebase I’m currently working on).

Now, I have a sketch for a workaround in my head which would be merely quite painful to implement rather than practically impossible.

But it would be nice to have a place to be able to discuss concerns like this. If there is one already, where? If not, can we get one?

5 Likes

(My feeling is that this .? operator sounds scary.)

There is library-level replacement for non-local returns that you can use like the following:

import scala.util.control.NonLocalReturns._

returning {
  ...
  foo.?
  ...
}
1 Like

@julienrf - Yes, I’m aware of the library-level replacement. I obliquely referenced it in my post (see “nonlocal-return-block”). It’s a bad solution for me because it involves manual work (forever into the future, too), but it’s better than no solution.

Anyway, the primary question is not about this particular thing (which is really bad for me personally), but about collecting many such things–here is this dropped feature, here are the workarounds for these interestingly different cases.

The Dotty docs often have very brief ideas about workarounds, but there’s no place that I know of to discuss them, or to collect solutions to more complex issues.

For instance, in my case, if I replace

var r = foo.?
if (rareCondition) xs.foreach(x => r = f(r, x).?)

mechanically with your suggestion, I will end up creating an exception when a local return could have done the job…and although some local exceptions get optimized into jumps, at least in the past I’ve found instances where they don’t and you pay a significant penalty.

It’s these kind of discussions about more complex scenarios that I think are missing, both to help out users who will need to port their code, and to inform people involved in implementing the changes who are trying to judge the actual impact of the change.


Also, FWIW, “scary” or not, .? is now my favorite single feature in Scala. It is hard to overstate how much it simplifies error handling while empowering meaningful reporting on the state causing the error.

3 Likes

How is .? different from just using exceptions? Is it just that ? is reflected in the type and exceptions are not? Would the difference go away if exceptions were checked?

Unrelated to the question, but where did you find these minutes? Looking at the SIP minutes page
(https://docs.scala-lang.org/sips/minutes-list.html) I can’t see anything more recent than January.

1 Like

It’s waiting for review/merge https://github.com/scala/docs.scala-lang/pull/1656

2 Likes

I don’t want to digress this thread for discussion on this particular feature, so I’d mention that the differences between ? and checked exceptions were already discussed in the pre-sip for checked exceptions thread. There’s no need to go into the details of the SIP, as the relevant discussion starts in this comment. There is also the thread on rust-like results, which I haven’t gotten to read yet but may contain some discussion on this topic as well.

Ah I missed that part. I am actually not sure whether .? is always or even usually faster than exceptions. It depends obviously on the relative frequencies of the regular and exception cases.

.? provides early return for the error branch of a method that returns a value with an error branch (e.g. Either[ParseError, Int]). It desugars, in my macro, to something identical to Rust’s ?, except because Scala has nonlocal return, you can .? out of closures.

For instance, code to open a file, read all lines, and parse each line of JSON into a Foo, while returning if there is any error, is simply (approximately, given appropriate other extension methods):

val lines = file.slurp.?
val foos = lines.zipWithIndex.map{ case (line, i) =>
  Json.parse(line).to[Foo].
    mapLeft(e => s"Cannot parse line ${i+1}: $e").?
}

If you had checked exceptions and monadic catching and manipulation of exceptions, I guess it would be sort of similar. Even the best application of cats/scalaz monadic error handling tends to look inferior to me (and induces a lot of cognitive overhead).

The great thing about .? is that it can both capture local state (not just the stack, but you can supply the relevant data) and it bypasses all the control flow within the method (which sometimes may be quite elaborate).

Because it is exactly a return, it is never slower than exceptions. When it can be local, it is a jump, which is faster. When it can’t be local, it is precisely a stackless exception, so it isn’t any slower than an exception.

It does have to be handled at every method, so for very deep call chains it can be worse than an exception, but in practice I find I am rarely or never anywhere near the number of jumps where it makes sense to do the tradeoff the other way around.

1 Like

That’s true, but you also have to factor in the overhead of creating the Left /Right values.

On this note I think the javascript BabelJS project brought quite some value into the JS ecosystem by allowing seamlessly and selectively to adopt features which were going through the review stages. Arguably we don’t need it as we can hack and iterate Dotty directly but this gradual selective properties by themselves are good value add.
We have observed already wrt to the imports proposal that Scala and it’s tool ecosystem steers away from preprocessors, maybe a plugin system for Dotty with a flexible way to enable disable features go a long way to enable rapid exploration and support of features which were dropped?

1 Like

@odersky - Of course; if exceptions are truly rare, then you want to use them and avoid boxed sum types for your errors. But in lots of cases the error branch is not that rare, and/or the time taken by the operation is large compared to the time taken to create a Right so you may as well do it that way. (E.g. slurping a file; or speculatively parsing some text as an Int just to see if it is one.)

In case you missed it, there’s http://dotty.epfl.ch/docs/reference/changed-features/compiler-plugins.html already.

1 Like