Proposed syntax for _root_

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

I think this is the best suggestion I have seen.

I’m pretty negative on breaking changes at this stage on something as basic as this.

The nested grouping is a natural extension all by itself, and I’d want it even if I didn’t care about absolute imports. For instance, at my company:

import com.stripe.{
  foo.bar
  baz.bang
  thisAndThat
  andTheNext
}

5 Likes

There is one alternative that I don’t think has been raised yet: parameterize import. If it looked like a method with a single implicit parameter, then you could

import           my.thing._
import(relative) bippy.quux
import(absolute) quux.bar.foo

The disadvantage of this is that import is not a method, relative and absolute should not actually be in the symbol table in scope, and even if it was a method you can’t pass arguments after a method name without parens.

The advantage, however, is that it’s extremely obvious what is going on because ( and ) are not valid package names and visually separate the behavioral “parameter” from the imports, and because you can choose names that are actually meaningful in context. Furthermore, it’s possible to define more than two, if you want; so far we have discussed three behaviors (relative, absolute, and preferAbsolute). But there are others. For instance, you might want to import only symbols that aren’t already in the table, e.g. import(ifMissing) java.util._ to avoid shadowing scala.collection.immutable.List.

@Ichoran The only issue with this proposal would be that it cannot be used in an expression position. Otherwise, I like the idea of having at least import[relative] and import[absolute].

A workaround in expression position would be

val foo = bar({ import[absolute] my.shadowed.thing; thing })

But that’s pretty unsatisfying. Then again, that kind of shadowing is pretty unsatisfying to begin with.

Using x.y in an expression is not just about shadowing. It’s for when you want to use x.y without importing everything from x. As @nafg explained, that’s how you access fields and methods.

A fully qualified access like .my.foo.bar avoids worrying whether my is shadowed or not, so you can use it even when nothing is shadowed.

FWIW, a starting dot reminds me of ::std::whatever in C++ — the initial :: also means “from the root”.

Let me spell it out for stronger effect:

In any language design, the total time spent discussing
a feature in this list is proportional to two raised to
the power of its position.
0. Semantics
1. Syntax
2. Lexical syntax
3. Lexical syntax of comments

EDIT: I also enjoy relative imports, also from subpackages. Simplified example from actual code, off the top of my head:

package ilc
package feature.booleans

import feature.sums //relative to ilc
//... define booleans in terms of sums ...
1 Like

Let’s create a Scala Improvement Proposal (SIP) for a tree imports.

Do you allow multiple levels?

For example:

6 Likes

While we’re at it, why not generalize the syntax to expressions?

// Write:
System.{ out.println(lineSeparator) }
// For:
System.out.println(System.lineSeparator)

OCaml has a similar syntax:

> let mapmap f = List.(map (fun xs -> map f xs)) ;;
val mapmap : ('a -> 'b) -> 'a list list -> 'b list list = <fun>

// for:

> let mapmap f = List.map (fun xs -> List.map f xs) ;;
val mapmap : ('a -> 'b) -> 'a list list -> 'b list list = <fun>

Pascal also has something similar (the with keyword).

4 Likes

Not being able to compose imports is a right pain. So often I want a set of imports available in every file of a package.

3 Likes

Allowing multiple levels seems both sensible and natural.

Imports are really flexible. For example:


case class Add(a: Int, b: Int)

object Eval {
  def apply(op: Add): Int = {
    import opp._
    a + b
  }
}

I’m aware of that (same thing with OCaml BTW). Still, I would prefer to write:

def apply(op: Add): Int = opp.{a + b}

That’s more fluent and more natural IMHO (in addition to being shorter). And it seems like a logical extension to the import syntax you propose.

2 Likes

It looks rather confusing to me. And I guess this would only work for cases where literally everything you need is a member of the same path. For instance:

object foo {
  val a = 1
  val b = 2
  val c = 3
}

def bar(f: (Int, Int) => Int) = foo.{ f(a, b) + c }

I think the only consistent way to desugar this would be foo.f(foo.a, foo.b) + foo.c.

How often would you actually be able to use that?

No, it’s just syntax sugar for the full import form, quite simply. Something the compiler can do while parsing into the AST:

def bar(f: (Int, Int) => Int) = foo.{ f(a, b) + c }
// i.e.,
def bar(f: (Int, Int) => Int) = { import foo._;  f(a, b) + c }

Notice how OCaml does it in my example. The f that is referred to is not from the List module.

2 Likes