Updated Proposal: Revisiting Implicits

For me the key difference between Haskell and Scala is as you say, Haskell only has type classes, so they are likely be widely used and understood by the programmers writing code in Haskell. But I’m not convinced that is the case in the Scala domain. I suspect that most Scala engineers come from an OOP background and hence are more familiar with OO, and less familiar with type classes.

Also, it looks like both Rust and Haskell do use a keyword to identify a typeclass instance (i.e. instance or impl).

1 Like

I’m not sure about “lens Monoid” construct bit, I would rather it could work without this, or just use “object Monoid”.

Presuambly your extension syntax needs a tweak to bind the variable name “xs” to the extended class. Or perhaps you intended to use “this” instead?

I’m also not sure that an extension should “extend” a class, but that might be okay.

Otherwise, I quite like the syntax for " typeinstance … implements … "

First of all, I really love where things are heading towards.
I find the summary of “several classes of implicit” a few comments above by M. Odersky very useful, even for me as a beginner to get the idea behind this full rewrite.
I also like the idea of separating concerns, and having extensions as their own separate thing. Easier to Google, easier to look for in a code base, even easier to setup “tech debt rules” or stuff like that for code analyzers (like: “not to much extensions in Int, please…”).

The single sentence “conversions can be seen as special cases of typeclasses” helped me a lot to understand the idea, too thank you.

Speaking of that, I agree with some of the comments above: the same way “Conversions are a special case of type classes”, I’m wondering if “typeclass are implemented using given” could maybe be hidden?
That definitely has drawbacks like introducing a new keyword for “something that can already be achieved using existing mechanisms” (hence being noisy).
But I find quite interesting to have a concept properly labeled and identified: a typeclass, then looking at the implementation (“oh, actually it simply relies on anonymous given instances, so look: it looks like a given instance, but for a type, OK, it all makes sense now”).
I had to read the doc multiple types and typing these lines so that it “clings” to my mind: a typeclass “instance” (as in Haskell) is a “given class” (as in the docs) acting as as a given at type level, instead of value level (as given instances do). But I’m not sure I really understood what the object Monoid { ... } is about, it’s confusing to me.

Now I think I understand the sentence a bit better: “Typeclasses are just traits with canonical implementations defined by given instances.” from the docs. But maybe (and I agree with some previous comments) I shouldn’t dive into the “survivor bias” and forget about how I struggled first (especially since I think I get why there’s an object definition but I’m not really sure).

Maybe a typeclass keyword or typeinstance is too much. Maybe just a few notes on the docs or a good tutorial may help, too (that’ll always help in every case, anyway).

I’m not that much concerned about trait. Trait means “some definition of a type, a behaviour” (to me, at least) so it doesn’t confuse me that typeclasses are defined (as in Rust) with traits. But I do agree that Rust has impl, Haskell has instance whereas Scala seem to have a combination of an obejct and a given class. object seem to be part of the definition of the typeclass (you’ll be able to create an instance by defining a given class), but… I’m hesitating. Defining the typeclass through a maybe type trait, type class or something around these lines?

I understand this is maybe not the most helpful comment, if it sounds confused, that’s because I am.

But truly, the last re-write (of the syntax, and the docs) mentioned in this thread already helped me a ton, thank a lot for this. There’s just this typeclass thing that bugs me a bit. I think I got the concept from Haskell, but here the object part confuses me.

Thanks.

EDIT: I should have read typeclasses-new.html from the doc and not the old version. Now it’s given as and not only given

Yeah, it’s mostly an object-like component for the purpose of modularity, and the main reason for the separation between the two is so that those “implicit interpretations” (extensions, type instnaces, conversions) won’t be applied automatically when importing. You can read about this more in my proposal.

Yep, it was supposed to be this, I forgot to change it. I think it should still extend a class because (a) it is an extension, and (b) this prevents extending pure generic types (extension MyExt[A] extends A). Again, you can read about it more in my proposal :slight_smile:

I wouldn’t personally mind if methods and functions could be unified in a single concept at the language level. But as it stands I guess you could also say that functions in Scala are “simulated using a mixture of methods, classes and objects”. Given that a function f is actually an object that’s an instance of a Function class with an apply method.

1 Like

The thing is they are not actually a special case of type classes – at least not type classes in their original meaning – and for two reasons:

  1. Conversions work on concrete instances, while type classes work only on bound generic types.
  2. Conversions work without invoking any explicit function.

If conversion was really a type class you would’ve need something like this to use it:

// 1. explicitly asking for `Conversion` type class
// 2. explicitly invoking the type class via `apply()` or `()`
def convert[A, B](a: A)(implicit conv: Conversion[A,B]): B = conv.apply(a)

That’s exactly my point. Scala functions are simulated by other lower-level constructs, but that is mostly irrelevant to the developer and will likely hinder their work. A lot of clarity is gained when new constructs are designed for frequently used patterns that could’ve been implemented with existing lower-level constructs – that’s the core idea behind any non-assembly programming language.

1 Like

Agreed. But IMHO typeclasses don’t require a lot of extra ceremony on top of what’s already available in dotty. Adding too much extra sugar on top might confuse things more than anything else, but I know that opinions differ widely on this point. Anyway I came here to point out that the state of the art is already virtually equal to what @rgwilton suggested, modulo the keywords. I don’t think I’m the right person to defend all the given stuff, given that I wasn’t the biggest fan myself, though I must say it’s grown on me a little.

2 Likes

Ah, what I’m actually hinting at is that we don’t need those additional low-level constructs. We are not using them in any way that is not to specifically for these design patterns; hence, we really only need the sugar.

Fair enough :slight_smile:

Yes, exactly this.

Quite a lot of things in Scala end up being syntactic sugar to make code easier to read or write even though they can equally be expressed using lower layer constructs. I would regard typeclasses as being important enough that adding a little syntactic sugar to make them as simple as possible to read and write worth a bit of extra sugar.

4 Likes

wrt. 0.22 plans:

given [T: Ordering] as Ordering[Tree]

Is it just me or does this really read absolutely nothing like what it actually does?

What’s the 0.21 equivalent, the => syntax? I think that’s orders of magnitude more intuitive. There was this issue with it where some confusion would potentially be present when considering given function types, and I kinda hoped there’s going to be an improvement to it, but I liked @odersky’s reasoning wrt. this and thought its pretty fine. To me this feels like a step backwards.

edit: striked through likely incorrect understanding, but the usage of as I still find rather confusing. I guess my misunderstanding just demonstrates the fact :slight_smile:

1 Like

Speaking of the => syntax though:

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

Still prefer the 0.21 => syntax to its actual replacement. If we have to use with here though, wouldn’t

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

both read more naturally and give some intuition wrt. what this does due to its similarity with given parameters?

(removed the as keyword too, as per my previous comment.)

I’m going to read “Brideshead Revisited” and wherever it says “Brideshead”, insert “Implicits”.

And for “World War 2”, understand “Scala 3”.

Wikipedia doesn’t always cover fiction with perspicacity, but it seems to me that this summation applies:

It occurs to him that the efforts of the builders – and, by extension, God’s efforts – were not in vain, although their purposes may have appeared, for a time, to have been frustrated.

Except where it says “God”, insert “Odersky”.

One can google “Brideshead implicit evidence”:

the evidence produced that there is implicit in Brideshead Revisited an heretical private religion

Perhaps someday an apostate will write a critical history of Scala.

What I find most distracting is with or given when outside the parameter block. I disagree with the benefit/drawback analysis here. One form of ambiguity is being traded for another (within block vs between blocks), and it adds what I find to be strange keywords in locations I expect members – another stumbling block when parsing. It just feels like there will be more “Scala Puzzlers” as a result of this than placing with/given/implicit/bikeshed in the parameter block.

Other than that, the most recent proposal is pretty clean. I would prefer : over as but understand why that has issues.

I was going to replace “Brideshead” with “Bikeshed”, but to each their own.

3 Likes

I was a big fan of the syntax in the 0.21.0-RC1 release, it just seems so expressive:

given [A,B]: (Show[A], Show[B]) => Show[(A,B)] =
  (a,b) => s"(${a.show}, ${b.show})"
2 Likes

what I’m sad about is that in the newest proposal, the Context bounds are back, ala

def maximum[T: Ord](xs: List[T]): T = xs.reduceLeft(max)

There just shouldn’t be more than one way to introduce contextual/given/implicit parameter. That’s difficult to understand/explain, especially to newcomers.
Something like this should be enough

def maximum[T](with Ord[T])(xs: List[T]): T = xs.reduceLeft(max)

And it also scales nicely, when you realize you actually need to name the parameter

def maximum[T](with T: Ord[T])(xs: List[T]): T = xs.reduceLeft(max)

Also, it works for multiple-parameter type classes, something Context bounds just can’t do

def asdf[A, B](with Convertor[A, B])(xs: List[A]): List[B] = ...
1 Like

Note: they can, though it’s currently a bit awkward:

def asdf[A, B: [B] =>> Convertor[A, B]](xs: List[A]): List[B] = ...

which we’ll be able to write like this in the future:

def asdf[A, B: Convertor[A, _]](xs: List[A]): List[B] = ...

But otherwise I agree with your points.

1 Like

Note: Context bounds were always part of the proposal. The argument of extra syntax has to be weighed against the arguments of conciseness, convenience and backwards compatibility. Maybe if we never had had context bounds there would be no compelling reason to introduce them now, since the syntax of context parameters got so much lighter. But since context bounds exist, and are found useful, there’s less incentive to get rid of them. They are still a big improvement over context parameters for simple cases.

Note, in terms of my feedback for typeclasses, I’m not at all wedded to the existing implicit syntax (which I have generally avoided due to its perceived complexity), except that I superficially prefer the term “implicit” over “given”, but I understand the reasons why you cannot reuse that.

But what I am hoping for is a bit more syntactic sugar for defining typeclasses, and more uniformity for defining extensions. I would expect to use both of these features so having a clean, obvious syntax for defining them would be a win in my opinion.

Perhaps a specific syntax for defining typeclasses could mean that context bounds would mostly not be needed and potentially could be deprecated?

1 Like

A specific syntax of typeclasses has been proposed and pursued for years before the present implicit work started. In my opinion this is a dead end. It is a strength of Scala that typeclasses are regular types and instances are regular terms. You are free to disagree but you will never convince me otherwise.

4 Likes