Updated Proposal: Revisiting Implicits

That’s less a strength of given managing the semantics than it is a weakness in how expressive lambdas can be.

This looks like it should go away when/if Issue #4712 reaches consensus (aside: I’m really looking forward to this one)

It’s nice because you get a free lazy val for all givens with type parameters that have no value parameters, e.g:

Actually, no. givens with type parameters are also translated to defs. When I wrote “parameterized”, I meant both type and value parameters. So to share all instances, you’d have to do a construction similar to the one in Scala 2:

private lazy val monoidList = new Monoid[List[Any]] { ... }
given Monoid[List[A]] = monoidList.asInstanceOf

To say the truth, I have lost understanding this logic. Of course I can learn it by heart. But I can’t understand where there is simplification. It is strange simplification when someone should learn by heart how to use such simple stuff. I am sorry but currently I can see more disadvantages. I can say it because it is not the first reaction, a lot of time has gone.

1 Like

I doubt it. New abstraction is pretty leaky, i.e. you need to know how it translates to lower level constructs to control the initialization order, access performance and number of objects instances. New abstraction also requires new (somewhat incompatible) syntax whereas implicits worked directly on top of what was (and still is and will be) available in the language. def, val, lazy val and object stay in the language and will continue to be used extensively so Scala programmers need to know the difference between them and their purpose anyway.

Removal of a single short keyword (sometimes two) from a given definition is a too small gain for the created confusion and inflexibility. OTOH the reduced ceremony with extension methods is substantial and obviously warrants new syntax.

7 Likes

As for redundant keywords, I think nested givens may be unnecessary. Below code:

given listOrd[T](given ord: Ord[T]): Ord[List[T]] { ... }
given [T](given Ord[T]): Ord[List[T]] { ... }
given (given outer: Context): Context = outer.withOwner(currentOwner)

could be shortened to (given keyword for parameters omitted, because it can be always assumed):

given listOrd[T](ord: Ord[T]): Ord[List[T]] { ... }
given _[T](Ord[T]): Ord[List[T]] { ... }
given _(outer: Context): Context = outer.withOwner(currentOwner)

and that would read better.

With the current scheme (implicits in Scala 2) marking a parameter of implicit def itself implicit makes sense because that differentiates implicit values from implicit conversions. Such differentiation is redundant if implicit conversions are handled through scala.Conversion type instead.

I actually have mixed feelings about this proposal (removing nested given keyword), but it feels certainly a lot better than omitting def, val, etc

3 Likes

I like the regularity of this system, but it seems to me that regularity is broken in (5) and (7), e.g. given [T]: TC { defs }. The normal usage of : is name: Type, but here there is no name, only a type parameter. Why is the : there in these variants?

3 Likes

But why? Sharing pure polymorphic instances is also what Haskell does. A non-inline given cannot change because of different type parameters – it must have value parameters to change because types are fully erased – it’s exactly the same as a non-parameterized given.

EDIT: I don’t think the singleton type cast trick is relevant, unless DOT specifies semantics for reference equality? You MUST opt-in to insanity for it to lead to unsoundness – it can also be detected and require a langauge flag.

There would have to be caveats. You could or should not share if

  • there is a context bound
  • the class is specialized on a primitive type

So, it would be fragile. It’s better to have dead-simple rule: if there are parameters it’s a def otherwise it’s a lazy val.

Context bounds are value parameters, that is not a caveat, but follows directly. And AFAIK Dotty does not implement specialization yet, anyway?

Context bounds don’t look like parameters, and specialization might be implemented later. We need rules that are dead-simple, and stable under all circumstances.

1 Like

Wouldn’t: “given def|val|object ... behaves identically to every other def, val, or object” be considerably simpler than the current syntax?

As a bonus, it’d be one less set of rules to learn

2 Likes

I believe no, not at all. The problem is we all come from the mechanism side and worry about it now. But it does not matter!. Compare with how typeclasses are defined in Haskell. There’s no indication what these things compile down to, because, again it does not matter. People trust that their Haskell compiler will do the reasonable thing without having to worry about details. We have to free ourselves from the old thought patterns that are too close to mechanism.

7 Likes

I strongly disagree with this assertion.

Without some sort of control over how the typeclass instances are materialized, we have no recourse when compile times grow unpleasantly long. Currently, enforcing caching of intermediate branches of implicit lookups to avoid duplication is one of the primary tools for bringing down compilation times.

Under the currently proposed syntaxt, this part of the abstraction leaks because each form has defined materialization semantics, appending val|def|object after given just gives those semantics a name that’s familiar to reduce confusion.

5 Likes

If I understand correctly could this actually be big issue if due to cross thread initialization + the fact that Dotty now requires you to explicitly annotate lazy val as volatile? Basically in non trivial cases it appears that given can cause problems with variable access patterns (which is the whole reason why we have lazy val/val/def).

Agree, and if you do add lazy val/def/val to given its just pretty much a syntax rewrite of implicit, in which case I would prefer the effort to go towards fixing the quality of life issues with implicit.

No, that’s not true,

Normally, you should not worry about what happens. But in the rare case where this does matter, you do have complete control over the process. Whenever you want to ensure some particular runtime behavior, you can define your abstractions normally, and then inject with a separate given alias. That’s all.

1 Like

Let it be always def. It will be dead simple.
I just don’t understand motivation so
It seems for me that all confusion follow from virtual rules.
A declarative language helps when it solves a complex task when there is the separation of abstractions. Which task does solve current approach?

1 Like
  • the fact that Dotty now requires you to explicitly annotate lazy val as volatile?

That’s no longer the case.

1 Like

Fair enough but there are cases where you want a plain val and given doesn’t allow this (current implicit’s allow this).

There is also a difference between a 0 arg def and a lazy val (one is memoized, the other is executed every time). This difference also matters in non trivial cases and it appears given doesn’t have any control of this.

I just forsee a lot of issues due to this.

1 Like

To get a plain given val:

val x = whatever
given T = x

The given in this case translates to a forwarder def since the right hand side is a simple pure reference. So you have defined one value plus one forwarder, which will be inlined by the JVM. Generally, the rules have been designed so that the straightforward strategy to define your value normally and then add a given works as expected. There’s one exception to this that @AMatveev brought up, but I believe most people would classify it as an abuse of implicits anyway. That’s when you want to construct a given over a mutable variable. You have to do it like this:

var x: T = ...
given [Dummy]: T = x

Without the [Dummy] parameter, you’d get a lazy val which would not track future updates to the variable. But, I think a system that constructs givens that change according to variable mutations is dubious anyway. So I don’t mind that getting this right requires some more advanced knowledge.

1 Like

People trust that their Haskell compiler will do the reasonable thing without having to worry about details. We have to free ourselves from the old thought patterns that are too close to mechanism.

This, 1000x. :clap:

2 Likes