Updated Proposal: Revisiting Implicits

Perhaps someone should split off a separate thread about the keyword related discussions so this thread can stay on topic.

4 Likes

There was an example that had given not in the last part of the parameter block. I think this should be disallowed, because

def confusing(given Foo, foo1: Foo, foo2: Foo = Foo.default) = (summon[Foo], foo1, foo2)

can be called as

confusing(myFoo, yourFoo)

and although it would need to be written

confusing(given myFoo, yourFoo)

to fill the 1st and 2nd parameters instead of the 2nd and 3rd (so it’s not ambiguous), I think it is, well, confusing.

So I think we should only allow implicit parameters to go last. Also, they should be called “given parameters” if we’re using given. Pretty confusing otherwise. Then again, it’s also confusing if you call them that, because it sounds like you’re explicitly giving them.

implicit really is a very good word to use for this; it has all the right connotations.


If I were redesigning this from scratch, I don’t think I’d even have a separate idea of a given parameter. Rather, I’d use a trait to indicate that the parameter should be filled in.

def example(foo: Foo, bar: Given[Bar]) // Named parameter
def example(foo: Foo, _: Given[Bar]) // Not named

Then we only need to use standard mechanisms to create these things.

To create instances, you just do it normally;

val myFoo = Given(new Foo { def bippy = "bip! bip!" })
val _ = Given(new Bar { def message = "you can't name me, just summon me" })
object bazzy extends Given[Baz] {
  val instance = new Baz { def explanation = "the thing you give is in def instance" }
}
lazy val _ = Given(new Quux { def hi = "Hi!" })

To use the parameters, you get an automatic conversion from Given[A] to A, so you just use them. (You can also use instance to get the instance explicitly.)

For parameterized givens, you

def fiddleOption[A](fiddle: Given[Fiddle[A]]): Given[Fiddle[Option[A]]] = new Fiddle {
  def act(oa: Option[A]): Unit = oa.foreach(a => fiddle act a)
}

I’m not sure the above is a good idea in practice. It represents a large change, late in the process, and would require a large and sometimes awkward migration from existing Scala 2 code. But I think that anything additional that can be done to regularize the usage of givens is a good thing.

I do like the new framework more than the existing implicit one. It seems more principled and better-directed at the task. But we still have non-standard syntax and clunky grammatical forms, which could be improved. The alias/instance thing is also not great as it stands.

1 Like

No, there is a serious misunderstanding here.

def confusing(given Foo, foo1: Foo, foo2: Foo = Foo.default) = (summon[Foo], foo1, foo2)

cannot be called

confusing(myFoo, yourFoo)

What wording in the docs led you to believe that this is legal?

2 Likes

I love the idea that givens/implicits are not to be named! Not in declarations, not in parameter lists :+1:

given Ord[Int] = ???

def sort[A](given Ord[A])(list: List[A]): List[A] = ???
1 Like

I love the idea that declaration of implicits/givens should have different keyword than passing an argument explicitly!

I would be even OK with keeping implicit and pair it up with explicit.
This is beautiful:

implicit Ord[Int] = ???

implicit ExecutionContext = ???

def max[T](implicit Ord[T])(x: T, y: T) = ???

val intOrd = summon[Ord[Int]]

max(explicit intOrd)(x, y)

Putting aside the discussion over the keyword of choice (and whether there should be one or two new keywords), which I believe is a critical issue in this discussion, I still think there are a few elements in the new proposal which would make implicits even more confusing than they are now.

1. New “define”

The new proposal has given as a new definition keyword; i.e, in some contexts, given has a similar semantic to that of val, def, object. It’s hard to think of given as a new “thing” in the language. Instead, it should be treated as a definition modifier, much like private or public:

given intOrd: Ord[Int] {
  ...
}
// is equivalent to a "given" object:
given object IntOrd extends Ord[Int] {
  ...
}

given listOrd[T](given ord: Ord[T]): Ord[List[T]] {
  ...
}
// is equivalent to a "given" generic def with anonymous instance:
given def listOrd[T](given ord: Ord[T]) = new Ord[List[T]] {
  ...
}

given global: ExecutionContext = new ForkJoinPool()
// is equivalent to a "given" val:
given val global: ExecutionContext = new ForkJoinPool()

given (given outer: Context): Context = outer.withOwner(currentOwner)
// is equivalent to a "given" anonymous function with return type:
given (given outer: Context) => { outer.withOwner(currentOwner)} : Context

2. New anonymous cases

The new proposal has given introduce new ways to define anonymous things that their non-given equivalent does not allow. This is confusing, as it creates more special cases just for the keyword:

given Position = enclosingTree.position
// equivalent is impossible (values must be named):
val Position = enclosingTree.position // would actually override value of "Position"

def maximum[T](xs: List[T])(given Ord[T]): T = ???
// equivalent is impossible (parameters must be named)
def maximum[T](List[T])(given ord: Ord[T]): T = ???
def maximum[T](_: List[T])(given ord: Ord[T]): T = ???

// is not in the proposal but was brought up in this discussion:
given [A](given ord: Ord[A]): Ord[List[A]] = ???
// equivalent is impossible (anonymous functions cannot be generic):
[A](given ord: Ord[A]) => { ... }: Ord[List[A]]

The additional anonymous-related feature that the proposal introduces only for givens is importing of anonymous values:

object A {
  given TC = ???
}
import A.{given TC}

This is impossible with non-given values / functions as those cannot be defined anonymously in an object.

3. New name space

One last ability that the proposal introduces is the creation of a unique name-space for given values:

object A {
  val a = 1
  val b = 2
  given c = 3
  given d = 4
}
import A._ // imports a,b
import A.{given _} // imports c,d

There is no need for that IMHO, as it could be easily replaced by writing the values in separate objects (name spaces) in the first place.

Furthermore, I stated earlier in this discussion my objection to automatically making given imports also given in the importing scope / context – it’s an additional side-effect that one has to consider.

object A {
  given i = 1
}
object B {
  def foo(given i: Int) = ???
  import A.i
  foo() // should fail
}

All in all, I believe implicits should remain a minimalistic feature, and should not add new abilities beyond the core feature of implicitly passing arguments to functions. Placing so many semantics in one or two keywords is extremely confusing, for both newcomers and existing users alike.

1 Like

I personally don’t think that (a) new keyword(s) is necessary. Yes, implicits are confusing, something new to learn how to use. You have to know about implicit context and about how priorities are resolved, where implicits are searched for, and so on.

But a new keyword (or a set of new keywords) will not change that. And as long as you can stay away from implicits, that’s fine. Unfortunately, that currently this is not feasible. Implicits are much too prominent and are even junior Scala developers will stumble over implicits very early on in their Scala career.

So, much more important than a new keyword and and new syntax to define implicits is to me the streamlining of the current use-cases for implicits, to make conventions and agreements into language features and move implicits more and more out of the hand of regular developers like me and to the background where the library wizards can to their stuff.

So, whatever the decision on the keywords (given, implied, implicit, provided, …!) , it’s my oppinion that

Implicits boilderplate should be hidden behind dedicated syntax for identifed use-cases

Ad-hoc poymorphism is one prominent use-case for implicits in Scala. There are conventions on how to implement, libraries that provide and compiler macros that produce all the complex machinery of implicits. The requirement to set up all the gears of the implicits machinery makes it brittle and fragile and prone to break. By identifying such use cases and implement the machinery in the compiler, we can ease the pain of using implicits and - since that machinery is now provided by the compiler - even streamline the use of these features, provide use-case specific help and guidance.

This leads me to the next point;

Implicits error messages should be meaningful and provide guidance in solving problems.

I cannot put into words the sheer frustration I have experienced with implicits resolution. I don’t often yell at my computer, but when I do it’s because of missing implicits and the cryptic error messages they generate. Implicits produced by methods that take implicits provided by implicit objects and somewhere along the way there’s a developer looking for the sweet release of death. It’s painful.

Having identified a use case for implicits (say, ad-hoch polymorphism), it’s essential that the error message produced in case of missing implicits are helpful and relevant for this particular use-case in question, not a generic message.

So, in summary,

  1. Extension Methods
  2. Implicit Conversion

These are awesome. We have some syntax to easily implement 1) and a principled construct for b). Love it.

  1. Given Instances
  2. Given Clauses
  3. Given Imports

Not so sure about these. They change some wording and semantics about implicits, but implicits will always be confusing and new and something that a newcomer into Scala will have to learn if they should run into it. I think there’s more merit in making sure that they will not run into it, as long as possible.

  1. Context Bounds

Have the changed? I don’t think so. Awesome, before and after. 10/10, would buy again.

I really appreciate the willingness to change and mold Scala into a powerful and simple language.

3 Likes

Here are the basic forms of the given instance syntax. There are three variables:

  • anonymous or named
  • direct instance or alias
  • monomorphic or generic

These give rise to the following 8 combinations:

  1. given TC { defs }
  2. given x: TC { defs }
  3. given TC = expr
  4. given x: TC = expr
  5. given [T]: TC { defs }
  6. given x[T]: TC { defs }
  7. given [T]: TC = expr
  8. given x[T]: TC = expr

That system is expressive and very regular. Several comments have recommended to drop some of these choices but the fact is that different commenters proposed different, and non-intersecting choices to be dropped. What’s redundant for one person is essential for somebody else. Following @jdegoes I want to defend in particular (1), i.e. anonymous, direct, monomorphic instances. Without them, any formulation of typeclasses is needlessly clunky. I believe the opposite choice (8) is also necessary: We need a way to talk about generic, named, alias instances, since this is the most general way one can define an instance. Once we have (1) and (8), it is quite logical to admit the full cube of choices between them. That is what the current system provides.

One issue that needs some adjustment is the role of the label x: . It is optional in all circumstances when we talk about givens, no matter whether it’s an instance or a parameter. This is very regular once you know the principle, but it can also be confusing when compared to other uses of : in definitions.

After having taught this in class, I found that students were confused about the second variant, since it looked like an abstract declaration to them. To be precise, in the concrete case there were no refining definitions after TC. The confusing code was just

  given t: Table

We could demand at least an empty refinement for such cases:

  given t: Table with {}

Maybe this is enough to avoid the confusion.

A more substantial change would be to change from : back to as (which we had before), but now only for direct instances, whereas aliases would keep :. Then it would look like this:

  1. given TC { defs }
  2. given x as TC { defs }
  3. given TC = expr
  4. given x: TC = expr
  5. given [T] as TC { defs }
  6. given x[T] as TC { defs }
  7. given [T]: TC = expr
  8. given x[T]: TC = expr

The principle here would be that given x as TC would behave like object x extends TC. It’s less regular than before, but does not have the problem of looking like an abstract definition. The one issue I have with it is number (5): In given [T] as TC { defs } the “as” looks out of place. True, that syntactic form is also problematic when using :, i.e. given [T]: TC { defs }. But the problem is somewhat less glaring than with as.

One way to solve that problem would be to drop (5) and (7) and to require that parameterized givens must always be named. That avoids some oddity at the price of reducing regularity even more.

One could ask, why not use extends directly instead of as. This would give:

  1. given TC { defs }
  2. given x extends TC { defs }
  3. given TC = expr
  4. given x: TC = expr
  5. given [T] extends TC { defs }
  6. given x[T] extends TC { defs }
  7. given [T]: TC = expr
  8. given x[T]: TC = expr

Unfortunately, this gives another set of false similarities. In particular, (6) looks way too much like a class definition for comfort. It makes it look like x is a type where in reality it is a term. So I would advise against that.

What are people’s thoughts? Should we value regularity or avoid similarities that could confuse?.

4 Likes

But 6 does desugar into a class and a def, and so there’s both a type x and a term x, the following compiles in Dotty right now:

trait Foo
class A {
  given x[T]: Foo {}
  val a: x[Int] = new x[Int]()
}

I think this needs fixing: it should not compile. x is a term, not a class.

2 Likes

I believe the proposal should not introduce either (a) new syntax for defining these combinations or (b) the ability to express combinations that are currently not possible.

Here is the list of what can achieved with current syntax and what’s not possible:

  1. anonymous, direct, mono: given new TC { defs }
  2. named, direct, mono: given val x = new TC { defs } or given object X extends TC { defs }
  3. anonymous, alias, mono: given {expr}: TC
  4. named, alias, mono: given val x: TC = expr
  5. anonymous, direct, poly: ???
  6. named, direct, poly: given def x[T]() = new TC { defs }
  7. anonymous, alias, poly: ???
  8. named, alias, poly: given def x[T](): TC = expr

I might also agree with @jdegoes that only anonymous combinations should be allowed, in which case we will be left with a far more concise syntax, which would also fit much better with the two distinct keywords model: imply for putting something in the “implication cloud” and implied to define a parameter that draws an implied value from the “implication cloud”:

  1. anonymous, direct, mono: imply new TC { defs }
  2. anonymous, alias, mono: imply {expr}: TC

Three comments:

(1) We need something that is easy to teach. In the current given system, that’s the case. Here is how I would formulate the rules:

A given instance for a type TC is written

given TC

followed by either a block that provides definitions of members TC, or an alias = expr. If the instance has a name or parameters, these come between given and TC, and they are followed by a :.

I would not know how to teach the syntax you propose effectively.

(2) I really would like to retire new. It has an operational meaning that is out of place for term inference.

(3) The most important thing in the instance definitions is TC, the type for which we define an instance. It should always come before the details of how it is defined.

4 Likes

I personally do not understand without documentation where is

  • object
  • val
  • lazy val
  • def

It is a big secret for me how it works with threads. I do not think that “lazy vals” should always be thread safe

1 Like

It seems to me quite easy to teach that give or imply is an operation that makes a value an implication, much like throw throws an exception, return returns from a function, etc. I think it is extremely easy to grasp this concept as an instruction, not as a definition, which makes it very easy to teach.

Well that’s just how new anonymous instances are created nowadays; if Dotty replaces this with a new syntax, then given should follow the same.

I understand and agree with this line of thought, but it’s just that anonymous expressions do not allow this syntax. If they were, then given could just follow the same. Actually, this problem exists in other cases as well:

def foo(i: Any): Int = { i.asInstanceOf[Int] }

This is very similar to the problem with implicits, because in both cases there is a requirement to re-define the type of a value to fit the declaration of a function – the type of a parameter with implicits, or the return type in the other example. In both cases, which I would guess are quite rare and a quite the code smell, one would need to declare the new type at the end of the expression.

1 Like

This is a great overview! Personally I quite like the changes proposed so far. I think removing all the possible combinations of def, val, lazy val and object are an improvement. Also having a dedicated syntax for extension methods makes type classes and extensions methods themselves a lot nicer to work with.

That said I would probably be in favor of some irregularity in favor of ergonomics. So either extends or as in favor of a colon and a direct definition. And lastly, I still favor using two keywords over one. Makes code a lot easier to visually parse.

In the end though, the most important improvement will likely be better error messages. My biggest frustration with the current implicits are figuring out what exact implicit I am missing somewhere along the line of dependent implicits.

3 Likes

I believe that @Ichoran made a brilliant suggestion, that nobody commented on.
The main idea is defining implicits using basic building blocks everyone knows (val, def + type / typeclass), and removing implicit keyword

I see it as a solution to

  • the problem of naming the implicits (new keyword),
  • learnability of implicits,
  • explorability (ctrl+click),
  • easy readable syntax everyone already knows
package scala.compiler.di


type Implicit[A] = A
// this could be trait Implicit[A], i made it a type alias to suggest that it "disolves" from the bytecode, it's just a compiler mechanism, that affects only the compilation and not the generated code
/**
 * here goes a fantastic description about how implicits are searched/provided by the compiler during compilation
 * (Ctrl+Click) on Implict[_] takes you here (explorability)
 */
object Implicit {
   // constructor
   def apply(a: A): Implicit[A] = a
   // materializer
   def summon[A]: Implicit[A]
   // if you don't like the naming you can define your own extension methods that will suit your needs better
}

// you can define your own if you really would need different names from
type Given[A] = Implicit[A]
type Implied[A] = Implicit[A]
type Context[A] = Implicit[A]
type DI[A] = Implicit[A]

How to define and use implicits

trait Ord[A] {
   def compare(x: A, y: A): Int
}
object Ord {
  val int = Implicit(new Ord[Int] { override def compare... })
  // or perhaps ?
  val int: Implicit[Ord[Int]] = (a,b) => a < b // ?
  // i am not the fan of this one but it's ok
  object int extends Implicit[Ord[Int]] { ... }


  // this is very verbose, but others would also be
  def tuple[A,B](ordA: Implicit[Ord[A]], ordB: Implicit[Ord[B]]): Implicit[Ord[(A,B)]]

  // all arguments are implicit because the return type is Implicit[_]
  def tuple[A,B](ordA: Ord[A], ordB: Ord[B]): Implicit[Ord[(A,B)]]
}

//
package example.usage

trait SomeCollection[A] {
  // what used to be 
  def sort(implicit: Ord[A])
  // would be
  def sort(ord: Given[Ord[A]])
  // or the user could use the type alias he prefers
  def sort(ord: Implict[Ord[A]])

  def mixed(i: Int, ord: Give[Ord[A]], s: String)

  def notmixed[A](i: Int, s: String, ord: Implicit[Ord[A])

  def example() {
    mixed(1, "22") // should not compile  since the arguments are mixed
    mixed(i = 1, s = "22") // works -> when mixing implicits with regular args, user would be forced to use by name arguments

    notmixed(1, "22") // works (if the implicit is found)
    
    notmixed(1,"22", Ord.int) // provide implicits explicitly seems quite obvious
  }
}
2 Likes

This doesn’t make code more safe. But it makes code more fragile at least in context of dependency injection. it doesn’t allow to manage thread safety. It seems this is not cheap improvement.

1 Like

I think that real improvement it is not cutting functionality. It is availability of features which allow users do not think about “implicits” at all. it is main advantage of kotlin scope managment.

1 Like

Should we value regularity or avoid similarities that could confuse?.

After thinking a bit more about it I come down in favor of regularity. The point is, normal uses of given instances of class (2) are not confusing. E.g.

given intOrd: Ord[Int] {
  def compare(x: Int, y: Int) =
    if x < y then -1 else if x > y then +1 else 0
}

This cannot be confused with an abstract def since there is a concrete method definition right in the middle of it. The Table example I gave earlier is confusing, but can be easily rewritten to an alias:

given t: Table = Table()

I do not think that syntax design can rule out by itself alone all possible cases of confusing code. This is ultimately the responsibility of the programmer. All syntax design can do is:

  • Avoid confusions in common usage
  • Offer ready alternatives if some special case ends up being confusing

The current design meets both requirements.

2 Likes

I’m strongly against this suggestion, and for these reasons:

  1. Implicit is not a type. It is not a characteristic of an instance. It’s a temporary / contextual state in which an instance can be in.

  2. I’d rather have a dedicated keyword than a magical trait that does more than just being a trait. This is the same as with DelayedInit, which although being useful has this kind of magical abilities.

  3. This would require making implicitly defined instances automatically implied in the importing context. This is an invisible side-effect (unless you bother reading all the source code you import), which in my opinion is one of the major reasons I implicits are so confusing nowadays, even for veterans.

  4. This allows to define an implicit class, which really makes no sense. I believe it was agreed that extension methods should be handled differently.

1 Like