Proposed syntax for _root_

My friend made a compiler plugin for the syntax you propose (except it uses ⊢ instead of a dot): ohnosequences/contexts. For example:

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

With exactly the same meaning as you propose: { import foo._; ... }

3 Likes

Judging by the likes and the comments, the following next steps look like the best way to move this initiative forward:

  • Make _root_ a keyword once and for all.
  • Lobby with Intellij and language servers to prettify _root_.

It seems that the idea of the tree import has gained some traction and, after a while thinking about it, it now seems reasonable. A plus that hasn’t been discussed is that will minimize GitHub diffs if it is combined with trailing commas.

I’ll write two SIPs for these changes. If anybody wants to join me, ping me.

5 Likes

There was a pretty active discussion about this topic… some time ago. :slight_smile:

1 Like

Note that everyone uses imports incorrectly.

You can’t use util unqualified without an import because it is always imported by scala._, modulo -Yno-imports. Cf this example.

Here’s another example from scaladoc of egregious relative imports. There are packages model.diagram and page.diagram, not quite sibling but like cousins once removed. We’re in package page. For

import model._ ; import model.diagram._ ; import diagram._

it was relying on a bug to prefer page.diagram to the imported model.diagram. I prefer the corrected behavior, because I can’t tell by looking at the code that there is any such package page.diagram, and also the relative imports as shown are confusing to me anyway.

If voting is still open on the previous discussion, I like tree syntax and also the expression syntax. I don’t like having to add braces to accommodate an extra import x._. Even better if it desugars to stabilizing

{ val v = x ; import v._ ; body }

1 Like

Then what about going full-in and add the underscore for relative imports?

import fully.qualified._
import _.relative.name

even though for object and local imports it starts to get clunky

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

import _.Bar._
object Bar
1 Like

That would make it impossible to use relative imports in sources cross-built across 2.12-2.13/2.14. Ideally, any changes should be 100% backwards compatible and not prohibit cross-building.

3 Likes

It seems that the Rust team has added the nested import groups to 1.25 that @MasseGuillaume was lobbying for.

// on one line
use std::{fs::File, io::Read, path::{Path, PathBuf}};

// with some more breathing room
use std::{
    fs::File,
    io::Read,
    path::{
        Path,
        PathBuf
    }
};
1 Like

seriously, how do I unsubscribe from this thread?

4 Likes

4 Likes

Hi, has anyone worked on a SIP?

1 Like

I’d be interested in discussing a SIP (as I missed the discussion the first time round).

I’d like to add the following points:

I’ve been by people working on Scalac and Dotty that the compile-time cost of supporting relative imports is disproportionate to their value. The reason, as briefly explained to me, modulo my recollection, is that in a language with only absolute imports, the process of naming is a quite trivial lookup in a limited number of imported scopes, all of which are fully-qualified, whereas permitting relative imports requires some amount of typechecking to be completed in order to resolve names, and naming and typechecking become much more interleaved.

I agree, therefore, with @olafurpg’s suggestion that absolute imports should be “the default” and relative imports should come with a “syntax cost”. The claim that there’s a compile-time cost for relative imports is very much consistent with the comprehension cost for the programmer.

I think that the syntax _root_ is very ugly, and should be removed from the language. There’s no other precedent anywhere in Scala for giving something a name surrounded by underscores, and it is an anomaly. It would be great to replace it with something more consistent with the ergonomics of Scala. Historically, IIRC, it was chosen so as to not require the introduction of a new special keyword (root) and to not be likely to clash with existing keywords in use. Dotty doesn’t have so many qualms about introducing new keywords, so I don’t think this applies any more.

My preference would be to deprecate _root_, and to make absolute imports default, and import _.foo seems like perfectly reasonable syntax for relative imports.

If we took this approach, there’s no reason why we couldn’t have a long transition period where both syntaxes are supported, before rewriting the compiler to take advantage of these promised compile-time speedups…

I broadly support the idea of being able to nest imports, not so much because I think it’s going to be really useful, but because the Principle of Least Surprise says they should be composable.

Tangentially, I heard a suggestion somewhere, some time ago, that Scala would be more understandable if imports did not introduce implicits into scope, unless prefixed with the implicit keyword. For example, you would require implicit import scala.concurrent.ExecutionContext.Implicits.global if you wanted to have it available in scope to use implicitly. I have no strong feelings about this. (I think it would be a radical change with both advantages and disadvantages, which would more or less balance out…) But I raise it here in case other people want to discuss it.

That’s all I’ve got.

2 Likes

John Pretty Jun 1 '18

I think that the syntax _root_ is very ugly

I agree, and I think aesthetics may matter more than one may expect. For me the appearance of Scala programs was one of the reasons to start working in it.

My preference would be to deprecate _root_, and to make absolute imports default, and import _.foo seems like perfectly reasonable syntax for relative imports.

I agree; but since a lot of arguments have been given here pro and contra I give some quotes:

olafurpg Sep '17

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.

lihaoyi Aug '17 and Sep '17:

If you use package.foo.Bar.doSomething(...), the package keyword makes it abundantly clear you’re not using a local symbol. In fact, it is clearer than having some import 1000 lines up at the top of the file and then trying to figure out which Bar you have in scope to call doSomething with.

I use libraries fully-qualified all the time

I agree with olafurpg and lihaoyi here. But nafg wrote on Sep '17:

(…) 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.)

I think this case is not that strong, since import expressions are quite different from value expressions. As an import expression a.b.c would preferably denote an absolute path while as a value expression it would be allowed to be relative. In comparison, I find the case for keeping dots as infix operators in expression syntax much stronger.

So I prefer to have both

  • _. for relative imports
  • package. for absolute value expressions

About the roadmap:

pagoda_5b Mar 14 '18

Then what about going full-in and add the underscore for relative imports?

olafurpg Mar 14 '18

That would make it impossible to use relative imports in sources cross-built across 2.12-2.13/2.14. Ideally, any changes should be 100% backwards compatible and not prohibit cross-building.

Alternatively we could postpone the change to version 3.

John Pretty Jun 1 '18

If we took this approach, there’s no reason why we couldn’t have a long transition period where both syntaxes are supported, before rewriting the compiler to take advantage of these promised compile-time speedups…

IMO better: have a compiler switch for which the default value will change at some future version. This way the speedup would become early available.

Thanks for the summary, Andre! Scala 3 has a “Scala 2 mode” which could be the means of transitioning between different syntaxes.

Also note that package objects are currently compiled to an object whose name is package, and it’s currently possible to refer to it with,

`package`

so something else might need to change to support this. I also don’t like the idea of using a keyword as an identifier. It’s already done with this and super, but at least in those cases, those keywords are not used in other positions.

1 Like

A few miscellaneous thoughts:

Are we talking cost in terms of compiler complexity, or in terms of speed? If there is a major compiler-speed win to be had here, that’s worth thinking about.

That said, while I think it’s plausible to make absolute the default for imports, I am much more skeptical about that being the case for values, and we seem to be overlooking that aspect. The language is currently consistent that I can do both:

import scala.collection.mutable
import mutable.Map

and:

import scala.collection.mutable
...
val myMap = mutable.Map

I can see the first going away, but really not the second. I suspect that the supporting the second would require keeping most of the compiler cost, but I’m open to being told otherwise. More generally, this is my problem with the idea of changing the syntax of relative imports: it makes them inconsistent with the syntax elsewhere.

Agreed. While I don’t think it’s terribly critical, it would be a nice enhancement to the language. (And would reduce the pain of getting rid of relative imports.)

I’m pretty skeptical that this actually makes much difference in terms of comprehensibility – implicits are a mental hurdle to get over, but this hasn’t come up often in my experience as an additional one. And given how important imported implicits are for typeclasses, I’m very leery of messing with it, unless we wind up with a typeclass approach that doesn’t require it…

2 Likes

Hi Mark!

I should be cautious about passing along vague promises about compiler performance, as (at the very least) it would require some significant compiler refactoring to reap the supposed benefits, but the claim is that the compiler would be less complex, which would present itself as a performance enhancement.

Regarding values, it wouldn’t be necessary to stop supporting your second example in order to benefit from the reduced complexity in the compiler. The logic goes that every directly-referenced identifier in a given scope would have to resolve to an identifier in a (probably small) set of absolute imported scopes, none of which should require (much) typechecking to resolve, as compared to relative imports which might require typechecking of the symbol being imported from in order to know what’s inside it. In theory, that’s a tiny cost-saving which accumulates for every single directly-referenced identifier being resolved.

I think the benefit from my last point (I don’t want to call it a suggestion… :wink: ) is for those cases where you have an implicit being used, and you have no idea where it’s coming from. If you could reduce the number of imports which could be introducing it to just a couple, that could save a lot of time for checking. You could test it by removing the implicit prefix from one of them and seeing if it makes a difference, for example…

I agree that val myMap = mutable.Map cannot go away. And it would be terrible if we invented different name resolution conventions just because an expression is used in an import. That would fly in the face of Scala’s language design principles. So, that sinks the whole proposal, I am afraid.

3 Likes

@odersky Don’t we already have a disparity in that expressions can contain unstable paths, while we can’t import from them?

So then one would have to write:

def foo(ctx: Context) = {
  import _.ctx._ 
  ???
}

This looks silly, but the main point I would like to make is: how would I intuitively parse the first _? The only other place where you would see this syntax is in lambdas (list.map(_.foo)). So is it a placeholder? A placeholder for what exactly? ctx is a “fully qualified name” here; it has no prefix. Or would that mean that import ctx._ is still legal? That only relative imports from non-local names receive the weird syntax? That would kind of defeat the purpose though, because then I can still for instance shadow package scala with a local val.

4 Likes

@odersky Don’t we already have a disparity in that expressions can contain unstable paths, while we can’t import from them?

I don’t really see how this compares. Some expressions need to be stable – not just imports but also prefixes of type selections or pattern expressions or prefixes in a new. That’s a restriction. It’s not a change of the basic name resolution rules depending on what context you are in.

@Jasper-M
Yes, that example does look odd, and I’m not particularly fond of it, though my suggestion was also to discourage relative imports. FWIW, I still prefer seeing this to _root_ anywhere, in purely aesthetic terms.

I’ve always thought of the underscore as meaning “everything, something, anything or nothing”, or “whatever”. Its interpretation is highly context-dependent and it’s certainly in keeping with Scala’s philosophy for _ to be go-to syntax for cases like this. Or, to argue that another way, adding another overloaded use of _ when it already has about eight makes less of a difference to the language than introducing a new keyword, or reusing an existing keyword which previously only had a single use… However, in this particular case, _.ctx._, you have two underscores, with different meanings, more or less juxtaposed, which (I think) is unprecedented, so it probably counts as a new level of confusion.

Anyway I don’t want to mislead anyone into thinking that I’ve got any particularly strong opinions on any of this. I’d like to see alternative experiments, particularly if they come with some other benefits like faster compile times.

@odersky
It’s a moot point. I like that I can glance at Scala code, and if I see a.b anywhere, I know invariably that it’s referring to a member called b inside an term-level entity called a. And yes, there’s definitely value in knowing that a resolves uniquely in the current scope. My example of the case where a is unstable was an example of somewhere we accept (because it’s sensible to do so) a slightly weaker global invariant: we can’t say that import a.b._ imports the members of the result of the expression a.b; we have to constrain the universal “context-free” interpretation to stable paths. But I’m not going to flog this dead horse any longer. :slight_smile: