Updated Proposal: Revisiting Implicits

As far as I am concerned I hope that we have reached a fixed point. I am actually quite happy with the current proposal. If someone has a brilliant idea how to improve some aspect of it further over the next weeks I am happy to consider, of course. But my expectation is that we are by and large done now.

Generally, supporting votes like yours are very valuable in these discussions. I have found that, time and time again a proposal has very little support and a lot of negative comments. It’s only when the proposal is changed that the former silent supporters speak up in favor for what was by then lost. So to regain balance it’s also important that the people who like (some part) of a proposal voice their support.

2 Likes

I agree that given ... with ... as is the best of the proposed variants.

However I think that the witness ... given ... of form would be superior if one could find an alternative word to witness that would work for both typeclasses and context-passing.

The main reason is that a solid noun reads better when defining instances. The combination witness ... with ... of could in that case also be considered.

Btw I’m happy that this discussion continues. I agree it’s important to get it right and as such I think all possibilties should be explored and all aspects analysed before deciding that “this is as good as it gets”.

If it were up to me, I would put a “gag-order” on all discussions involving implicit syntax/semantics until after the 3.1 release. We should be focusing discussions and tooling and bugs!

only joking of course, but maybe I should hold back making slide decks like this: https://www.slideshare.net/pjschwarz/scala-3-by-example-better-semigroup-and-monoid :sweat_smile:

Given the rocky transition from the old SIP process, I have a feeling a gag order would be received extremely poorly

To clarify: I would understand a gag order of this type to mean, “The people with doubts about this syntax are to shut up until it’s too late to change anything.”

7 Likes

I see your point but I’m sure you understand my intention. I really, really want to see some closure on this topic. Otherwise I can’t justify spending days and weeks porting libraries to the new semantics.

I think given/with/as improves the situation. I still find it irregular, and I don’t like the tense being used, but it at least is quite clear what everything is, which is arguably the most important consideration. I no longer think that the syntax is a flat-out mistake. I really like that the with captures all the arguments in one block so there is zero ambiguity about whether a parameter is given or not.

I continue to think the underlying capability is good, but that the syntax (and the capabilities!) are insufficiently regular.

Anyway, I’ve said plenty about this before. Summary: thumbs more up than before, but with only modest enthusiasm.

Incidentally, with clauses can apparently be interspersed with regular parameter blocks. This seems a really bad idea to me. I’ve said this before, but I just want to make it clear that the new syntax does not in any way help.

def foo(x: X) with Y (z: Z = zero)

either can be called as

foo(x)(nonZero)

which makes it confusing as to what is an explicit parameter and what is given, or cannot and instead must be called as

foo(x) with (y) (nonZero)

which both makes the with clause pointless, as you have to supply it anyway, and has the strange construct (y) (nonZero) that looks like it ought to have tighter binding than to with, but doesn’t.

So I think with clauses ought to have to come last, after all normal parameter blocks. They add a lot of confusion without pulling much weight if they are allowed in the middle.

4 Likes

The question whether normal parameters can follow context parameters is still not quite settled. Until last night, the docs in the PR said yes but the implementation said no. The main reason for the “no” was that the example you gave is indeed atrocious:

def foo(x: X) with Y (z: Z = zero)

But there are at least two reasons why we still want to write regular parameters after context parameters.

  • some use cases get simpler that way
  • collective extension methods with collective context parameters translate to this pattern. E.g, last
    example in extension-methods.md:
extension on [T](xs: List[T]) with Ordering[T]) {
  def largest(n: Int) = ...
}

expands to the extension method

def [T](xs: List[T]) largest with (Ordering[T]) (n: Int) = ...

The revision last night allows context parameters after regular ones, but requires context parameters to be in parens in this case. I.e. the example you have would be illegal, you’d have to write

def foo(x: X) with (y: Y) (z: Z = zero)

instead. It improves legibility somewhat IMO, but I agree it’s still not great.

1 Like

Are there inherent problems with changing the troublesome expansion so that the given parameters come at the end?

1 Like

Putiing the context parameters at the end would rule out some parameter dependencies:

extension on [T] with (x: U) {
  def f(n: x.M) = ...
}
1 Like

Would it be possible to implement annotation to customize ‘given …’ when the default behavior does not suite well?
It is actual for

  • optimization( @threadUnsafe)
  • later initialization
  • reusage objects from pool

There are methods to overcome it. But readability of such methods scares.

1 Like

I guess that’s supposed to be called with either of

xs.largest(5)
xs.largest with(myOrd)(5)

?

The latter looks really weird.

What about switching the given parameters to match the collective extension order instead of the extension method order? That is, it expands to

def [T](xs: List[T]) with (Ordering[T]) largest(n: Int) = ...

This has the additional nice property of making extension on a trivial mechanical rewrite, simple enough for anyone to do in their head.

In this case, the allowed syntax for an explicitly given parameter would be one of

(xs with myOrd).largest(5)
(xs)(myOrd).largest(5)

either of which seem okayish to me (I’d mildly prefer the former).

Then we have to consider the within-language-consistency of xs with t. On the one hand, it is a little bit confusing because it reminds one of creating an anonymous class with a trait mixed in, but that isn’t what’s happening here. On the other hand, functionally it is quite a close analogy; given parameters are use-site mix-ins, basically, whereas implemented traits are compile-site mix-ins. So it’s kinda okay?

Anyway, I hope we can avoid def foo(x: X) with (y: Y)(z: Z = zero). The parens prevent one from becoming utterly baffled, but it’s still counterintuitive and not particularly pretty.

It’s actually

xs.largest.with(myOrd)(5)

The . is mandatory.

What about switching the given parameters to match the collective extension order instead of the extension method order? That is, it expands to

def [T](xs: List[T]) with (Ordering[T]) largest(n: Int) = …

That is currently not allowed. The leading part can only have one (non-implicit) parameter. We could potentially generalize it, but it would take work to do so.

That’s unusual syntactically (since xs.largest is not an instance), but it’s clearer than what I suggested / imagined. I’m not sure it’s better than an extra set of parens (i.e. xs.largest(with (myOrd))(5)).

I’m assuming that xs.largest(5) is also okay? So if you wrote

class C {
  def biggest with (x: X) (i: Int) = ???
}

you could or could not call it as c.biggest(5)? If no, then you shouldn’t with xs.largest(5) either, which kind of torpedoes the utility of extension methods. So the answer must be either, “no, but this is irregular”, or “yes, you can call it that way”.

Maybe it’s not worth messing with it more. With clauses followed by normal parameter blocks are syntactically kind of a wart as it stands, but it’s probably livable-with as a somewhat awkward corner of the language that most people can stay away from.

I’m assuming that xs.largest(5) is also okay?

Yes, of course,

I’m sympathetic to this concern, however I think it makes more sense to spend the time to get to something everyone is Ok with, rather than port to the proposed syntax now, then port again after 3.1.

I really hope we have not reached a fixed point. Like many before me, I believe this proposal as it currently stands (which is not inherently different than how it started) has a negative net value, especially given its motivation – to simplify implicits and make them more approachable.

I started playing around with a complete (theoretical) alternative proposal, partly based on many ideas of others that were brought up in these discussions (@LPTK @lihaoyi @LukaJCB to name a few). There is a discussion about it here.

The general idea of the proposal is to completely break the abstraction of “implicit” / “given” and provide each feature – dependency injection, type classes, extension methods and type conversions – its own syntax, while also introducing a way to resolve “implicit interpretations” locally.

I would really like to hear the opinions on that proposal from people who opposed many aspects of this current proposal, as I believe my proposal answers many of their reservations. I would be interested in the opinions of FP veterans especially (@alexandru), as type classes are much more commonly used in that community.

1 Like

I think this usage of as is still a bit confusing. Have you considered using extends instead?

given ord extends Ord[T] { ... }

This is similar to the existing object definitions.

By the way, this raises a question. Consider the following type class:

trait Ordering[A] {
  def compare(a1: A, a2: A): Int
}

And the following instance:

given orderingInt as Ordering[Int] {
  val foo = "bar"
  def compare(x: Int, y: Int) = x - y
}

Is it valid to write orderingInt.foo? Do we create an anonymous subclass of Ordering[Int] when we implement this instance?

The expansion of givens is explained here. So the answer is yes, you can write orderingInt.foo.

I believe as is much better than extends. In my previous comment I explained how the as syntax literally translates into spoken language. extends is doubtful already for objects (making a value extend a class is a stretch).

This might be an ignorant question (and I suspect that it has already been debated to death), but it seems that “as” is just giving a name to a given. But in Scala, we can already name things that don’t change (i.e. val)

E.g. rather than …

given ord as Ord[T] { ... }

, is there a reason why …

val ord = given Ord[T] { ... }

… isn’t sufficient, particularly if naming givens is expected to be the exception rather than the norm?

Thanks,
Rob

1 Like