Transparent term aliases

To make scala.collection.immutable.List accessible without import, package object scala defines both a type alias and “term alias”:

type List[+A] = scala.collection.immutable.List[A] // Transparent
val List = scala.collection.immutable.List // Opaque

This is a commonly used technique – in scala, scala.Predef, and in many libraries. So far so good. However, the former alias is de jure, whereas the latter - only de facto. The type is transparent, the val is opaque - you shouldn’t look at the right-hand side, and even if you do, there’s no (formal) guarantee that it will stay the same.

This is not a problem for compiling and running the code, but is a problem for editing the code - how an IDE would know that the List term is actually scala.collection.immutable.List? And vice versa: if you look for usages of scala.collection.immutable.List, you probably also want to see usages of List. We may add heuristics, but that’s hardly elegant.

While we now have both transparent and opaque type alias members (in Scala 3), there’s no way to define a transparent term alias. Although imports (inlcuding ones with renaming) are transparent (if you navigate to an imported name, you navigate to the defintion, not to the import statement), they are not members, and so are not “exportable” outside the scope:

import scala.collection.immutable.List // Not exported

exports are “exportable”, but they are not transparent (?):

package object scala {
  export scala.collection.immutable.List // Opaque
}

Interestingly, a val is effectively transparent if it’s a constant value definition – a final val without a type annotation and with a constant expression as the RHS. However, constant expressions don’t seem to include objects (?), which is strange, given how this works in the literal-based singleton types. Even though object is effectively lazy val, it’s transparent, because the RHS is synthesized by the compiler.

Can, in principle, the language treat a reference to an object as a constant expression (at least for top-evel objects), and the standard library make scala.List, as well as other wanna-be-transparent term aliases, final? Then Scala code can have transparent term aliases, which can be correctly handled by IDEs. (The key here is the semantics; whether the compiler replaces LHS with RHS in the bytecode, as it does with literals, is an implementation detail.)

5 Likes

Wouldn’t

inline def List = scala.collection.immutable.List

work as a transparent term alias? @nicolasstucki

Ideally, it would be great to also support Scala 2 (especially given that Scala 3 reuses the standard library).

Wouldn’t this achieve what you want?

final val List =  scala.collection.immutable.List

Yes, that’s one part of it. However, the language also needs to treat scala.collection.immutable.List as a constant expression, because that’s required for the val to be a constant value definition, and currently this doesn’t seem to be the case (?). We don’t even need to change anything in the compiler, only for the specification to explicitly say so :slight_smile:

But, in practice, just adding the final modifiers may be already sufficient - an IDE can rely on that to detect the intent.

Maybe it should be

inline def List: scala.collection.immutable.List.type = scala.collection.immutable.List

to make sure the type is not widen

transparent inline def List = ...

could be enough.

1 Like

inline / transparent inline seems to fit, thanks! So transparent term aliases should be possible in Scala 3.

(As for Scala 2 and the standard library – we may still at least make those vals final.)

IIRC the only thing that’s special about a constant value definition is that the val will receive a singleton type instead of a widened type (since there is syntax for singleton types for primitives and strings the concept is a bit redundant). But in val List = scala.collection.immutable.List the val will have a singleton type either way. So it’s not clear to me what would have to change or why.

There is a formal guarantee. Its type is s.c.i.List.type. There’s only one possible value. You don’t have to inspect the RHS to know that.

1 Like

Good point. However, there’s more than one possible value, because singleton types also include null (at least in Scala 2):

val List: scala.collection.immutable.List.type = null

Besides, there could be no value:

val List: scala.collection.immutable.List.type = ???

So we cannot think of List as a compile-time constant just because of the type. Technically, there should be an additional, formal guarantee for that. (In practice, however, we may consider the presence of a singleton type good enough.)

From Inline | Macros in Scala 3 | Scala Documentation it seems that literal-based singleton types have been an indirect means to enable inlining (which doesn’t include objects) :

The final modifier will ensure that pi and pie will take a literal type. Then the constant propagation optimization in the compiler can perform inlining for such definitions. However, this form of constant propagation is best-effort and not guaranteed. Scala 3.0 also supports final val-inlining as best-effort inlining for migration purposes.

Scala 3 now explicitly uses inline rather than final to define constant values:

The Config object contains a definition of the inline value logging. This means that logging is treated as a constant value, equivalent to its right-hand side false. The right-hand side of such an inline val must itself be a constant expression.

There’s an explicit restriction that “only constant expressions may appear on the right-hand side of an inline value definition”:

inline val List = scala.collection.immutable.List
// Error: inline value must contain a literal constant value.

In Scala 2, it’s possible to write:

final val List = scala.collection.immutable.List

It’s not formally a constant value definition, but at least there’s a clear intent.

To make this a compile-time constant, the specification should say that a reference to object is a constant expression (regardless of whether the compiler actually inlines it in the bytecode). Then we would have a formal way to define transparent term aliases, even in Scala 2.

To see why this matters in practice, consider that, after the collections redesign in 2.13, most references to collections (Traversable, Iterable, Seq, IndexedSeq, Iterator, List, Map, etc.) in Scala code are actually references to aliases (and this idiom has been adopted by other libraries).

This scheme was supposed to be an implementation detail, and to work in the same way as the implicit imports (scala._, scala.Predef._). However, now the navigation takes you to aliases rather than classes, the action to show documentation shows nothing useful, finding usages is problematic, even the syntax highlighting is different because of the aliases. All this obviously affects the UX.

To improve the UX in IDEs, ideally, we want:

  • a formal way to define a term alias (in contrast to implementing an ad hoc heuristic),
  • a way to express the clear intent (in contrast to, say, treating any accidental singleton type as an alias; just as treating any implicit Set[String] as an implicit conversion from Int to String),
  • something that is applicable to Scala 2 (because Scala 2 is still widely used, and the standard library is reused in Scala 3),
  • an approach that is future-proof (e.g. if we are supposed to use export for that in Scala 3, maybe it should be possible to add transparent).
4 Likes

If the main problem is documentation and IDEs, maybe we should just focus on that. Ability to “export documentation” via annotations for scala-doc may be enough.

1 Like

It’s not just documentation – here’s a list for the IntelliJ Scala plugin (so far):

All this stems from the semantics – the pattern with two aliases was supposed to be just another way to import things by default, but it’s very different from import as far as IDE is concerned (e.g. if you navigate to an imported name, you navigate to the definition, not to the import statement). If we had some standard way to define a “transparent export”, that would fix these issues automagically.

Of course we can (and will) try to work around this at the IDE side, but it would be great to also address that at the language side, if possible.

2 Likes