Proposal: Add @deprecatedReturn Annotation

Proposal

Add a @deprecatedReturn[A] annotation allowing you to specify a less specific return type for a method using the annotation’s type parameter. The type of the type parameter MUST be a super-type of the method’s current return type.

For example:

class User {
  @deprecatedReturn[Seq[Session]]
  def activeSessions: List[Session]
}

In the example, treating the return value from the activeSessions method as a List[A] would be deprecated, but treating it as a Seq[A] would not. If not typed explicitly, the compiler should prefer to type the return as Seq[A] if possible1.

Motivation

Sometimes, return types for methods in the standard library (or other libraries) are accidentally made unnecessarily specific2, significantly restricting and constraining the ability of maintainers to improve or optimise implementations and fix bugs3.

Creating a deprecation cycle for an unnecessarily specific return type is not actually possible for public methods, as you still want the method to retain its current, sensible name. If you want to deprecate the method, you need to do each of the following steps, while waiting for a new, binary-incompatible version between each:

  • create a second method with a different name and the less specific return type, and deprecate the old method
  • remove the old method
  • create a third method with the name of the original method and the signature of the second method, and deprecate the second method
  • remove the second method

I opine that the above steps are ugly, painful, and generally infeasible. Alternatively, you could just change the return type of the method in the next binary-incompatible version and hope that it doesn’t silently break anyone’s code. A @deprecatedReturn annotation would allow a simple migration to the less specific return type with warnings at any pace desired.


1. I’m not actually sure how this would work at the compiler level, though I do believe that the compiler currently prefers not to call deprecated methods when possible.

2. I don’t mean in any way to criticise contributors or maintainers who accidentally make return types overly specific; it’s easy to make such mistakes, and also easy for it to be the correct decision when written, but incorrect after some other changes.

3. scala/scala#9388

5 Likes

Related:

I don’t see any way how this could be implemented. The compiler cannot immediately know how the type is used. For example it could be assigned to a val without explicit type, and methods of the subtype would be called only further in the code. The compiler cannot backtrack all the way up to choose a different interpretation.

This gets much more impossible when implicits or overloads are selected down the line based on the subtype.

1 Like

I don’t immediately see any big problems with this. Do you have a scenario in mind where this would silently break something?

1 Like

Agreed – I’m not really seeing the use case. This is one of the situations where I don’t usually think of a deprecation cycle as necessary. I mean, yes – the wider return type could propagate outward due to type inference, but that seems like an edge case that I wouldn’t usually worry about…

A better use case (which I’ve edited into the original description) is migrating from a return type of List[A] to Seq[A] (or sometimes LinearSeq[A]). Returning a List in a public API forces your implementation to internally copy the collection to a List before returning it, potentially for no reason at all. There’s no particular reason to assume that the consumer of the API needs a List, and if they do happen to need one, they can just call .toList.

In general, I agree, and my original example wasn’t great for that, but List has a few methods that Seq and LinearSeq do not (e.g. ::, :::), which means that a change without a deprecation cycle is likely to break a LOT of code.

This problem is especially pernicious because new Scala users coming from Java assume that scala.collection.immutable.List is the immutable Scala equivalent of java.util.List, and unknowingly use that instead of scala.collection.immutable.Seq. Migrating away from this mistake can be quite difficult.

5 Likes

One way I can think of something like this breaking silently is if the change hid a complexity class change that doesn’t “break” the code as much as render it unusable.

If a bit of code does a bunch of prepending to something that returns a List, and that is silently updated to a Seq backed by a Vector or Array, it’s plausible this could cause intermittent timeouts that would be frustrating to track down as (from the user’s perspective) the code causing the trouble hasn’t actually changed.

2 Likes

It seems like, as far as the standard library is concerned, that’s what @migration is for. Perhaps that functionality should be generalized.

1 Like

does @migration work for things not in the stdlib? I’ve never tried

No not yet. It’s even private to the scala package apparently. I imagine its usage would be a bit clumsy for third party libraries, since knowledge about libraries and their versions sits in the build tool and not in the compiler.