Proposed syntax for _root_

I think absolute imports should be encouraged with lightweight syntax while relative imports should come with a syntax tax. Absolute imports are inherently easier to reason about.

One proposal: make fully qualified imports the default and impose a . prefix for relative imports

import fully.qualified._ // becomes import _root_.fully.qualified
import .relative.name // becomes import relative.name

this would extend to local imports and imports on the empty package

def foo(a: Foo) = {
  import .a._
}

import .Bar._
object Bar

Nothing should change how reference resolve outside of imports.

4 Likes

@olafurpg’s proposal is the one I like the most so far.

  • We should encourage absolute imports as much as possible.
  • We can migrate code from the previous scheme to the new one with the least changes to the code (assuming relative imports are not that common).
  • . is a prefix we use everyday in our file systems, and it has a strong connotation associated to a “relative” path, which is arguably what FQNs represent (some kind of virtual filesystem of code). Using . is the right call.
2 Likes

A major issue with @olafurpg’s proposal is that there is no way to write cross-compiling relative imports that work before and after the change. No amount of scalafix can fix this: if I migrate my code, it does not compile with the old version anymore.

2 Likes

I agree this is an issue, esp. given that syntax has remained mostly unchanged 2.10 through 2.13. It should be possible to provide a compiler or build plugin to translate the new import syntax for older scala versions, the JS community has been doing this for years with Babel. Although, I admit that’s a heavy solution to support this single breaking change.

That’s much too blithe. There are a lot of us who don’t use IntelliJ, and relative imports are common in many codebases. (Especially for subpackages of the current package.) I think you’re significantly underestimating the amount of code that would be impacted by the change you’re proposing…

2 Likes

I question this assumption. Seriously, I think folks are forgetting the complexities of it. For example, opening one of my files more or less at random, I hit:

import EditFunctions._

The current file is in package querki.editing, as is the API-shared EditFunctions object, which contains all of the data structures for the API. This is the concise and obvious way for me to import those structures.

Seriously, people are asserting that this usage is rare. As somebody who uses it all the time, quite deliberately, I have to question that…

3 Likes

Unqualified imports are harder to reason about than fully qualified imports, they require awareness of what symbols are in scope at the import position. Fully qualified imports on the other hand always have the same meaning no matter their position.

My proposal was not to ban relative imports, the proposal was to demand minor additional syntax for relative imports to trigger extra attention from the reader. import .EditFunctions._ (or some equivalent syntax) is a small price to pay IMO for the increased emphasis that EditorFunctions is pulled out from the scope instead of _root_. In the ModelDesignerPage.scala source file you linked, 15 imports (or 14 imports if you exclude import rx._) out of 17 seem to be absolute.

1 Like

My understanding is that the main problem is that anything that can be an identifier can be shadowed by another definition that is in scope, so we want to use a reserved word. package seems to be the best existing keyword for _root_.

I understand the resistence to adding a new reserved word, but there is a huge set of rare unicode symbols we can choose from. Some of them could metaphorically be interpreted as meaning “root” or “start”. Here are some that I think we could use, that I’d be shocked if introducing them as reserved words would break anything.

:checkered_flag: chequered flag, indicating the start of a race.
:cherry_blossom: cherry blossom, symbolic of beginnings in Japanese culture.
:sunrise: sunrise, beginning of the day
¡ inverted exclamation mark, used for the beginning of an exclamation in Spanish
a doorway to higher wisdom
:rocket: a rocket, beginning of a journey
:red_car: a car, beginning of a journey
:bike: a bicycle, beginning of a journey
:ice_skate: ice skate, beginning of a journey

I would include ‘a’, ‘ℵ’ or ‘α’, but programmers who speak many languages wouldn’t like them.

(There’s possibly some BS here. I used a quora post and a tattoo guide for some ideas.)

Another metaphor for _root_ is that it is the entire universe, from which you add a package path to indicate a specific subset.

infinity
proper subset
𝕌 universal set
join, your program is the import and the surrounding text
marriage of the import and your program
:eye: the all-seeing eye
sun
sun
:sunny: sun
:globe_with_meridians: earth

Some of these might not be good to use due to programmers who speak mathematics.

My last metaphor for _root_ is that it brings life or meaning to the lifeless or meaningless.

flower
:evergreen_tree: tree. I particularly like this one, as the package structure is a close metaphor for a tree. There are other unicode trees to choose from.
root (see tree metaphor)
ankh, symbol of life
:heart: heart, love
:love_hotel: love motel
:couple_with_heart_woman_man: love
:rabbit2: rabbit, fertility
:chicken: the chicken to your egg (I didn’t find an egg symbol, so this answers the question: the chicken came first)

1 Like

I get that – I understand why you want this change. What I’m challenging is multiple people implying that this isn’t a common usage, so the change isn’t a big deal. That is absolutely untrue for me – I use it all over the place – and I’m concerned about the migration challenge. I suspect I’m not alone in that…

2 Likes

You are not alone in that. Especially when it comes to code that requires source compatibility with older versions.

1 Like

Also, the leading . for relative imports doesn’t solve the problem of @lihaoyi (and many others) who want to use fully qualified paths in expressions. Unless you want every non-fully qualified path to have to be prefixed with a ., which IMHO would be madness. But if you require . in imports but not in expressions, Scala’s syntax becomes less consistent.

Apologies if I’ve missed this part of the discussion, but why don’t we migrate towards making _root_ a keyword? It seems like the least painful migration of the ones I’ve seen proposed here.

3 Likes

I think because it’s ugly - at least that was @fommil’s original concern.

Unicode symbols (especially emoji) are difficult to type, and don’t render on all systems.

1 Like

I suggest lobbying with one’s IDE vendor to fold _root_ into one’s preferred emoji (may I suggest :snowman:) for the remaining matter of esthetics. See also: Wadler’s law.

5 Likes

It’s not only a syntax issue. I’ve came to Scala from Java and haven’t expected that subpackages of current package would have precedence over root packages. It’s not only a source of astonishment (I’m referring to the well known principle here) but also IMO a most common cause that requires the use of _root_. When I type a FQN and it doesn’t work my first thought is that there’s some value in outer or super classes with colliding name.

Try creating subpackages named ‘scala’, ‘akka’, ‘java’, ‘com’, etc Your code in parent packages will suddenly stop compiling (and in order to make it compiling you would need to use the _root_ semi-keyword). I think giving subpackages higher precedence than root packages causes more pain than gain.

One thing we should keep in mind here is that there is a huge difference between the way say Java looks at imports and packages, and the way Scala does.

In Java, packages (like a lot of things) are “special.” They aren’t similar to anything else and simply have their own rules, including the ability to import things from them. Similarly, fields and methods are two completely different things.

In Scala, there are only two namespaces, terms and types. Packages, singleton objects, and vals/vars/defs (whether class members or local), are all just members of the namespace of terms. And importing is completely orthogonal: You can import any member of any stable identifier. So unlike Java, where the “argument” to the import statement is unique syntax, in Scala one can make the case that import's argument is … ok, not simply a path … but a “path pattern.” (Not quite “pattern” as in pattern syntax, as used in pattern matching, for comprehensions, and definitions, but something sort of analogous to it.) So System.identityHashCode(x) and { import System.identityHashCode; identityHashCode(x) } are both using the same rules, except that one is a superset of the other. In other words import is basically saying `any path that follows the following pattern is in scope and as if one had written it.

That was a bit rambly, but the takeaway is:

  1. @lihaoyi’s point that absolute vs. relative syntax should be consistent between import and expressions is not just convenience or elegance, it’s a lore more fundamental than that to the language. (Not saying it’s mandatary, but there’s a strong case for it.)
  2. @olafurpg’s suggestion breaks this, unless from now on _every single identifier has to be prefixed with something. If you write val x = 10; println(x) and not println(.x), then x should be the equivalent of currently writing _root_.x.

Another counterargument to @olafurpg’s perspective (which flows from #2 above) is that always in programming, an unqualified identifier is relative, and we prefix to give more context. To illustrate:

object A {
  val x = 1
  val y = 2
  object B {
    val x = 3
    // Now: If I want to refer to B.x, the norm is to just write x. If I want to refer to A.x,
    // well I have to write out A.x because of shadowing, but even for y, there's
    // more, not less, of a chance I'd want to write A.y instead of just y. Arguably,
    // shadowing only makes sense because we already see things that way
    // in the first place!
  }
}

The above holds (approximately, at least) whether A and B are packages, objects, or methods (except that then you can’t qualify by them).

The takeaway there is that when it comes to qualifying things in code, we always think from innermost outwards. So even though you can make the argument about taxing things you want to discourage, in reality it’s a lot more logical to make absolute explicit and relative default.

6 Likes

Just don’t make :snowman: ​a keyword or you’ll break sbt :wink:

3 Likes

I also appreciate how identifiers resolve consistently between imports and terms. However, I have many times been bitten by this behavior in practice.

Making _root_ a keyword won’t solve this problem since I don’t want my source files to look like this

import _root_.a.b
import _root_.c.d
import _root_.e.f
// ... 15 more lines of import _root_.a

For a language that already supports import a.{b, c}, this feels unsatisfying. Here is an alternative proposal: extend grouped imports to support arbitrary nesting.

import _root_.{
  a.b
  c.{d => e}
  k._
  ...
}
  • backwards compatible, don’t use the feature if you want to cross-build to older scala versions
  • natural extension of already existing functionality: import a.{b, c}
  • no overloaded keyword or delimiter semantics
  • quality of life improvement for people like myself who prefer fully qualified imports

To complement this change, we can make _root_ a keyword, although I’ve never experienced problems with this in practice.

9 Likes

I never would have considered the snowman. It’s certainly better than _root_, which is a code smell that reminds me of Python.

2 Likes