Proposal: Implied Imports

I believe that right now, you generally should not need to import your implicits. As mentioned, having the “imports wrong” is the #1 usability problem with implicits, since there is zero guidance from the compiler or otherwise to help you figure out how to fix it.

This means that implicits should as much as possible be defined in companion objects, to be brought in automatically. This is done in uPickle, Requests-Scala, OS-Lib, and other libraries I maintain, and works pretty well. Occasionally you need to define-and-import orphan implicits when you need a T[V] and both T and V are from external libraries, but those are uncommon cases and not something that you do “by default”

In fact, I’d go a step further and say wildcard imports:

import my.lib._

Are themselves a code smell, implicits or not. I would be very happy if we could remove this functionality and make people import things explicitly; in many other languages (e.g. Python, JS) wildcard imports are explicitly discouraged.

Name the things you want to import! Implicits being imported are a bit awkward because the imported name doesn’t get used, but good, importing implicits should be slightly awkward and not the seamless default! If the explicitly-named imports are getting hairy because the library needs a huge pile of random names and implicits imported to do anything, that’s very much a problem with the library.

If we decide to remove wildcard imports (big “if”!) we could end up with a syntax like:

import a.b.{c, d, e}
import a.b.{c, implicit d, e}
import a.b.{c, implicit d, implicit e}

to demarcate exactly which names we import are implicit, and which are not. I think that would be a pretty good state of affairs to get into, e.g. allowing people to import both the global ExecutionContext and the parasitic ExecutionContext for convenience, but decide which one they want to be implicit:

import scala.concurrent.ExecutionContext.{implicit global, parasitic}
import scala.concurrent.ExecutionContext.{global, implicit parasitic}
1 Like

I think this principle is too restrictive in how it’s expressed by the programmer. The programmer should want either to import all but implied instances, or all instances, including the implied ones (when using wildcards). I don’t understand the reasoning why we force the programmer to express two different import statements instead of just annotating whether or not the import includes implied or not.

What about implied exports (assuming export gets accepted into the language)?

I disagree. I think that we have messy imports due to lack of export. With export we can finally compose importable objects that are good as wildcard imports.

I think this is more common than you believe. Besides the canonical library of these for Cats and the Standard Library (alleycats), a trivial search brings up three independent projects providing this for Cats and Scalacheck. There are also additional projects along these lines for Cats & Scalatest, and equivalent projects if you replace “Cats” with “Scalaz” in the preceding examples.

That’s aside from the internal libraries designed to correct ergonomic issues with Scalatest, Scalacheck, Mockito, Anorm, and etc that I currently help maintain in my day job, which depend heavily on implicits to make them usable. Better discoverability and error messages would be very helpful, as currently we rely on an import convention similar to cats.

Making define-and-import more difficult would greatly increase the adoption barriers of these libraries, and removing this capability altogether would put us right back into the space of relying on the library designers to anticipate every use case. Based on the number of libraries providing this functionality, there’s clearly an unmet need which the library designers aren’t able to fulfill.

Some of them are great about trying to provide as many of these instances as possible (shout out to enumeratum, who do an exceptional job of this), but others wouldn’t be able to keep up (scalacheck is a good example of an excellent library with a dearth of maintainers). Regardless, given the number of possible connections between libraries, I don’t think it’s reasonable to put this burden on them in the first place.

What about implied exports (assuming export gets accepted into the language)?

They also get an implied variant, with the same semantics.

1 Like

I believe that most of the need for implicit imports today comes from extension methods, like the toOK method you describe:

implicit class EitherCanBeOk[L,R](private val underlying: Either[L,R]) {
  def toOk: Ok[L, R] = underlying match {
    case scala.util.Right(r) => Yes(r)
    case scala.util.Left(l) => No(l)
  }
}

In the future, if you want to make this method universally accessible, you’d define or export the extension method at the toplevel of your project. No implicit needed.

  def (underlying: Either[L, R]) toOK[L, R] = underlying match {
    case scala.util.Right(r) => Yes(r)
    case scala.util.Left(l) => No(l)
  }

So, let’s analyze when you would still need an implicit import by classifying the different kinds of implicits.

  1. Typeclasses: You need imports only for orphan instances, which should be avoided anyway if possible. If that’s not possible, a highly visible import is good since it alerts the reader that this code uses some orphan instances.
  2. Extension methods. Can be handled directly.
  3. Contexts such as ExecutionContext. If you have a root context, define it with the context class, as is done in ExecutionContext.global. If you want to override a third party root context with something in your project, this deserves a highly visible import.
  4. Capabilities, configurations. Same remarks as for contexts apply.
  5. Implicit conversions. If they are defined with the source or target type, no import is necessary. If they are defined neither with the source type nor with the target they are evil. Requiring a highly visible import is the least we should do in that case (in fact we currently require a language import in addition to this).

Are there other classes of implicits? It would be good to add them to the classification.

In summary, so far, it seems that in future well-designed systems implicit imports should be both rare and would profit from being very visible. That’s what this proposal achieves.

The same cannot be said for today’s systems because of extension methods implemented by decorators. That’s why the proposal exempts current implicits from the import implied tax. I.e. as long as you use implicit classes for extension methods, your users can get them with the same imports as before. But once a package switches to Scala 3 implicits, the import implied is required.

2 Likes

Are you also considering idea of importing something into implicit scope. Something like:

object ExecutionContext {
  val global: ExecutionContext = ???
}

import implied ExecutionContext.global

val a = implicitly[ExecutionContext]
1 Like

import implied ExecutionContext.global

You don’t need the import for that use case.

Well, it’s just an example. global is not defined as implicit (val global: ExecutionContext = ??? but not implicit val global: ExecutionContext = ???). We don’t need implicit keyword for such a case at all, because we can control implicit/not implicit scope only by a way how we import stuff. Just an idea from the top of my head, maybe a stupid one.

Ah yes, I overlooked that global in your example was not implicit. Yes, maybe. We considered that in the SIP committee and the general reaction was rather luke-warm. So, we can leave it for later as well.

2 Likes

I do not know how this cases can be named, so I just have tried:

  • serialization\deserialization
    • sql
    • orm
    • rpc
    • xml
    • json
    • etc

There are libraries which provide types and such library do not know about “serialization\deserialization” and there are libraries which provide “serialization\deserialization”.

How can such libraries be easily integrated?

It is not rare tasks in my practice. And it is ironic cases. I want to use implicits and I do not like it in such cases, so I don’t use it if I can :slight_smile:

1 Like

Do you categories cover scala.collection.JavaConverters?

Do you categories cover scala.collection.JavaConverters ?

Yes: they are extension methods, so will need no implicit support in the future.

@Matveev:

Third party serialization packages are typical examples of orphan instances. They require import implied.

It is very good news, is there any description of it.

IIUC

Ok, It is important to know.

Although It is a pitty that such popular library like anormsql can be considered as rare case.
Why cannot it be simplified as extension methods?

Again, can someone please explain the motivation behind the import implied semantics. Even if I agree that import should not import implied instances, why doesn’t import implied just import all instances including the implied ones? I see no viable use case for just a import implied A._ statement if it imports just implied instances in A (and even if we found such a use-case, we can always just created object implicits with the relevant implied instances).

Additionally, what will happen to the implicit conversion inside Predef? Will the Predef object be imported by default to include the implicits?

On a related note, was a syntax introducing a separate import selector instead of a whole separate form of import clause considered and rejected?

I mean something like

import some.path.{_, implied _}

instead of

import some.path._
import implied some.path._
2 Likes

I personally do not read the import section.
For me, the main idea will be to demonstrate library users that it is not good(rare case).

Someone can make checkbox which automatically insert implied keyword.

I am personally think that library user should not be punished. Such complication should be done only for library authors. But after all I agree that import implied is evil, so It does not matter how more\less it will be with implied keyword.

I only hope that someday there will be more abilities to avoid such cases(for example sql, orm).

1 Like

This could actually be an interesting alternative, since it allows a refinement (which is based on a proposal by @liufengyun). We can also allow something like this:

  import some.path.{implied for Ordering[_], _}

The semantics would be that the import classifier implied for T would bring into scope all implied instances of some.path that return values of type T. The short hand form implied could stand for implied for Any. What’s nice about it is that it does not just discourage misuse but also encourages virtue, where being virtuous means narrowing down your imports to specific members.

We already have wildcard imports and named imports. Named imports are “virtuous” whereas wildcard imports are considered “sloppy”, so we encourage to use named imports. But for implicits, named imports don’t work. In Scala 3, implied instances can be anonymous, in which case we can’t use a named import for them. Even in Scala 2 implicits often have intentionally obscure names in order not to risk name conflicts, which makes it very hard to remember those names for an import. So most people just use a wildcard import instead.

With this new feature, we could specify imported implicits by the types they implement, which is precisely what we care about.

2 Likes

While combine the imports is more succinct, make them separate might be more readable.

import a.path._
import b.path._

import a.b._ for Ordering[_], String
import b.c._ for List[_], File

Can we use it for extension methods too? import a.b._ for T also import extension methods for the type T. Otherwise, it will be difficult to find where do they come from, just as implicit classes.

If this is a new form of import selector then the ability to use it grouped together with others or separately comes naturally, doesn’t it?

1 Like