Allow "implicit" keyword to be used with "import"


#1

When writing JSON encoder/decoders I often find myself not wanting to annotate them as implicit, as there are often multiple possible implementation (each for a different scenario). I want the user to decide which one to use explicitly at the use site.

An example of this is JSON parsing for java.time.LocalTime. In some endpoints, having the second part (“12:00:15”) makes sense, while for other endpoints it doesn’t (“12:00”).

Defining my decoder implementations (note the lack of implicits):

object JsonEncoders {
  val localTimeWithSeconds: Encoder[LocalTime] = ...
  val localTimeNoSeconds: Encoder[LocalTime] = ...
}

In order to bring them into scope as an implicit, I have to name them since I need to use implicit val

implicit val encodeWithSeconds = JsonEncoders.localTimeWithSeconds

useTheImplicit(...)

This gets worse if I need to bring in multiple implicits into scope

I propose allowing the usage of implicit with import to, which yields the the following:

import implicit JsonEncoders.localTimeWithSeconds
useTheImplicits(...)

I think this is much cleaner, and importing multiple things as implicits are also much cleaner

import implicit JsonEncoders.{localTimeWithSeconds, instantDateOnly}
useTheImplicits(...)

Things to Discuss

“import implicit” or “implicit import”

I think either way works. Former may be less confusing for newcomers(?). The latter fits the pattern of ‘implicit val’, ‘implicit def’

Ban blanket imports

What will the behaviour be for import implicit JsonEncoders._?

In this case it’ll import defs and vals in the given scope as implicits.
I think we should ban this, as it is extremely dangerous. (Adding a val, def will have unforseen impact on any code that blanket-imported the current object/class)

Other remarks

Personally I try to avoid defining implicits if I can, and prefer using locally-scoped implicits
to make it clear what implicits are active in the current scope.
I believe this really helps prevent the wrong implicits from being used accidentally (e.g. ExecutionContext, Encoders, etc)
Having some syntatic sugar will help a lot.

TL;DR:

def sendToA() = {
  import implicit JsonEncoders.{encodeInstantWithSeconds, encodeLocalTimeWithSeconds}
}
def sendToA() = {
  implicit val instantEncode =  JsonEncoders.encodeInstantWithSeconds
  implicit val localtimeEncode =  JsonEncoders.encodeInstantWithSeconds
}

#2

Here’s a link to a related thread:


#3

For your usecase you could just as well put your implicit vals in an object that is not in the implicit scope of the types involved. Then users can still only import the ones that they need:

import JsonEncoders.{encodeInstantWithSeconds, encodeLocalTimeWithSeconds}

#4

“import implicit” or “implicit import”

first one expose import nature of line. I think it is more important than it’s implicitness.

Ban blanket imports like import implicit JsonEncoders._

it is extremely dangerous!


not sure if I like the idea. It does very small benefit (few words less) and does not simplify anything. Most types used as implicit arguments are designed to be implicit, and they are rarely used by hand. This is good idea to separate them for types to be used as implicit args and ordinary types.


#5

Yeah, @Jasper-M’s suggestion seems so good that I’m :-1: on the current proposal.

But it made me wonder about a different idea (tho pretty hard to migrate to): should one require import implicit for imports that include implicits conversions (or maybe even implicit params), so that it’s visible which imports affect not just name resolution but also implicit resolution? But Dotty wants to encourage implicit classes at the expense of other implicit conversions, which seems a different way to warn about implicit conversions.

The other problem is that most “typeclass instances” are “safe” implicits, unlike ExecutionContext or JsonEncoders.


#6

In what way “”“most “typeclass instances” are “safe” “””?


#7

Here’s the full definition of JsonEncoders I should’ve put up:

object JsonEncoders {
  val encodeInstantWithSeconds: Encoder[Instant] = ...
  val encodeInstantWithoutSeconds: Encoder[Instant] = ...
  val encodeLocalTimeWithSeconds: Encoder[LocalTime] = ...
  val encodeLocalTimeWithoutSeconds: Encoder[LocalTime] = ...
}

Sure you can group them like so:

object JsonEncoders {
  WithSeconds {
    implicit val encodeInstantWithSeconds: Encoder[Instant] = ...
    implicit val encodeLocalTimeWithSeconds: Encoder[LocalTime] = ...

  }
  NoSeconds {
    implicit val encodeInstantWithoutSeconds: Encoder[Instant] = ...
    implicit val encodeLocalTimeWithoutSeconds: Encoder[LocalTime] = ...
  }
}

But that’s assuming you actually have a logical grouping. In many cases you don’t.
(I might be misunderstanding you. Sorry if I did)

I actually had the same thought while writing up the proposal. Seems like an interesting idea

My intention is to make it ergonomic to use implicits in a safe and readable way. I think this proposal helps push Scala in that direction but that’s up for debate.


#8

I mean you can just have

object JsonEncoders {
  implicit val encodeInstantWithSeconds: Encoder[Instant] = ...
  implicit val encodeInstantWithoutSeconds: Encoder[Instant] = ...
  implicit val encodeLocalTimeWithSeconds: Encoder[LocalTime] = ...
  implicit val encodeLocalTimeWithoutSeconds: Encoder[LocalTime] = ...
}

Assuming trait Encoder[A] is not defined in the JsonEncoders object and not a subtype of type JsonEncoders. Then a user can just import those specific implicits that they need, just like you did with import implicit ... except that they don’t need to qualify the import as implicit.


#9

Ok… what about such encoding:

case class TimePresentationConfig(showSeconds:Boolean, formatter:String)

object Encoder {
  implicit def encodeInstant(implicit conf:TimePresentationConfig): Encoder[Instant] = ...
  implicit def encodeLocalTime(implicit conf:TimePresentationConfig): Encoder[LocalTime] = ...
}

currently there is planned to require additional import on user site when more powerfull implicit conversions are used:

import scala.language.implicitConversions

but it’ll turn on power mode across whole file and all imports we have.

Selective

import implicit scalatags.JsDom.all._

looks more constrained way of saying “I know what I’m doing”. It also could help to discover from magic comes from.


But then imports like

import implicit JsonEncoders.localTimeWithSeconds

are totally unrelated to proposed underscore variant.

We could distinct those two features by using reverse order:

//this makes weird implicit conversions possible 
import implicit scalatags.JsDom.all._
//this makes imported object implicit
implicit import JsonEncoders.localTimeWithSeconds

I still don’t think that above import is better than using current state of art:

//this makes weird implicit conversions possible 
import scala.language.implicitConversions
import scalatags.JsDom.all._
//...
implicit val encodeWithSeconds = JsonEncoders.localTimeWithSeconds

My opinion: it is rather not worth it! This feature does not solve any problem well. Existing possibilities are good enough.


#10

My opinion also is that this adds more complexity to the language where existing solutions are good enough.


#11

Now there’s runtime overhead where there was none.

What do you mean? import implicit is a syntax error in scalac right now.

That’s a good solution actually.


#12

True. You can still memoize this Encoders in encodeInstant.

yea… just copy past problem. I fixed that. Sorry.


#13

I think there is a better solution to the problem shown by @jatcwang. When deriving the JsonEncoder/Decoder from the type, the type should contain all needed information to do so. This is not possible with LocalTime. It doesn’t contain enough detail for the JsonEncoder/Decoder on it’s own.

In this case I think it would be best to use tagging or new types to add more detail.

object DateTags {
  sealed trait WithSeconds
  val withSeconds = Tag.of[WithSeconds]

  sealed trait WithoutSeconds
  val withoutSeconds = Tag.of[WithoutSeconds]
}

type LocalTimeWithSeconds = LocalTime @@ WithSeconds
type LocalTimeWithoutSeconds = LocalTime @@ WithoutSeconds

So you should then be able to implement your JsonEncoder as

object JsonEncoders {
  implicit val localTimeWithSeconds: Encoder[LocalTimeWithSeconds] = ...
  implicit val localTimeWithoutSeconds: Encoder[LocalTimeWithoutSeconds] = ...
}

The shown Example uses Tagging from Scalaz (https://static.javadoc.io/org.scalaz/scalaz_2.12/7.2.26/scalaz/Tag$.html). It should be possible to do the same with NewTypes https://github.com/alexknvl/newtypes

I assume import implicit could be useful, but for typeclass selection I would prefer adding more informtion to the type.


#14

Many typeclass instances are not only coherent but also “canonical” in the sense mathematicians use informally IIUC: there is one clearly correct definition. The instance for Functor[Option] should be an example but I’m sure there’s more. So I wouldn’t want to call attention to the import of this instance. But differentiating between instances that are “canonical” or not would be a task for library designers.


#15

These are all great solutions to my problem. Thanks :slight_smile: