Updated Proposal: Revisiting Implicits

What about a selection rule for type-parameters instead, as showcased in the context-applied compiler plugin?

def fn[F[_]: Applicative: Traverse, G[_]: Applicative]: G[F[Int]] = 
  F.traverse(F.pure(""))(s => G.pure(s.size))

def fn[F[_]: Applicative: Traverse, G[_]: Applicative] : G[F[Int]] = 
  summon[Traverse[F]].traverse(summon[Applicative[F]].pure(""))(s => summon[Applicative[G]].pure(s.size))
2 Likes

While this is true, keep in mind that it’s syntax sugar that many of us use heavily – they’re pretty central to even mildly FP-flavored Scala. Losing them would be a significant additional change for a huge amount of existing code, and would force people to change more habits, increasing the upgrade tax, for no particularly good reason…

3 Likes

Why mark trait as typeclass? Shouldn’t trait Monad[F[_]] extends Functor [F] suffice?

Note, I have happily used aspects of a functional programming style in Scala for many years without any knowledge that context bounds even existed as a thing :slight_smile:

I’m not advocating removing context bounds to make folks lives harder, that would be unreasonable.

But, if there a bit more syntactic sugar for defining type class instances, then it might be context bounds aren’t needed, or are only needed in a few corner case scenarios. If this ended up being the case then I would recommend that context bounds are deprecated and eventually removed, with existing users required to migrate to the equivalent long hand in those corner cases (which presumably could also be done via a rewrite script). My justification is only to simplify the language, particularly if it was to remove an obscure feature that is no longer used very much.

You may wonder why I think that it is reasonable to remove “context bounds” but “add new typeclass instance”, and for me there are two key differences:

  1. It would be easy for someone without full knowledge of the language to google for something like “scala typeclass instance”, but I suspect that it is much harder to google solely based on context bound syntax, without already knowing that it is called, even more so if “:” is given further meaning in Scala 3.
  2. If the syntax for typeclasses is lightweight enough then I think that it will be more heavily used in regular code, and hence naturally become more familiar to regular scala developers.

Of course, if context bound syntax is still frequently needed even if scala had syntactic sugar for typeclass instance definitions then by all means keep it. But be aware that it is an additional complexity to the language that is probably not intuitive to non Scala experts, and I see that as a negative to the long term health of the language.

1 Like

I still don’t understand fully what your proposal is. Do you propose adding “typeclass instance” as an alias for “given” if the declaration of the implementing class of the type of the given is marked with a “typeclass” keyword?

You say that in OOP patterns are normally marked with keywords too, but I’ve never seen that. What language has that?

So, I’m not saying that a trait has to be marked as a typeclass. But if there is extra code that always needs to be generated for traits that are used as typeclasses (e.g. a companion object with an apply method) then I would much rather add a single keyword like “typeclass” to the trait definition than have to write the extra obscure boiler plate.

Consider case classes in Scala. My understanding is that the “case classes” is still just a regular scala class (and associated companion object) underneath with some predefined methods, and some best practices associated with them.

I’m not suggesting that typeclasses instances shouldn’t be represented as regular givens underneath, I’m just saying that having a bit more syntactic sugar to define them might make them significantly easier for less expert Scala users to read and understand.

There isn’t. You don’t need an apply method for typeclasses at all. If writing summon[Monoid[Int]] instead of Monoid[Int] to summon a given instance really is a show stopper for people (and I can hardly believe it is, @smarter proposes

It is more a plea than a proposal. I’m saying that I personally find the proposed Scala 3 syntax for defining typeclasses to be too obscure. And in a vain way, I think that if I find the syntax tricky then I suspect that other developers will also find it tricky and obscure as well.

One answer is to say that fully understanding implicits/givens is a prerequisite to being able to read/write Scala 3 code. Another answer is to say that regular Scala 3 users shouldn’t need to understand typeclasses at all. Both of these answers are okay, but I think that they end up reducing the pool of individuals/companies who are willing to work the language. Of course, it is possible that I am just wrong, and then most Scala developers will be able to pick up, or figure out, the syntax on their own.

Note, I previously suggested that I would prefer a syntax that looks something like (but preferably without the context bounds if that is possible): Updated Proposal: Revisiting Implicits - #276 by rgwilton

I don’t follow, I think that most (perhaps all) of them do.

E.g. a class is defined using the “class” keyword. A singleton class in defined using “object, or perhaps static class”. A subclass is defined using “class … extends … implements …”. I.e. I think that keywords are used to define the relative structure of what is being built between classes, sub-classes, interfaces, traits, etc.

Why it is okay to a “case” modifier to class that creates a companion method with an apply function, but not have a “typeclass” modifier to a trait that does something broadly similar (i.e. act to remove boilerplate at the call site)?

The syntax does look like that. Except instead of “typeclass trait” you just write “trait”, and instead of “typeclass instance” you write “given”. At some point there was a proposal to write “instance” instead of “given”, but that was rejected because it could too easily be confused with a class instance.

A visitor pattern doesn’t have a keyword, nor does a builder pattern, nor does an adapter pattern. And neither does a typeclass pattern in scala. You don’t write visitor class, builder class, adaptor class or typeclass trait.

It’s not (in itself), and that it does is accidental complexity. If scala 3 will drop the requirement for using new to call a constructor, it doesn’t have to do that anymore either, other than for backwards compatibility.

The case modifier creates the equals, copy, hashCode and unapply methods too though, which offer much more convenience than just the apply method. If all it did was add an apply method, I’d be all for removing it too.

The discussion here assumes that everyone knows what typeclasses are. But is that really true? If I look at Haskell, typeclasses are complicated. There have been hundreds of papers written about them (I have been guilty of contributing to that flood mysef). If I look at other communities, I have the impression that people throw typeclass as a term around since it is cool, but it could really mean a wide spectrum of things. So, what is a typeclass?

4 Likes

I like the definition(type-classes-101-introduction):
type class is a programming technique that lets you add new behavior to closed data types without using inheritance, and without having access to the original source code of those types.

1 Like

I probably have a simplistic, and perhaps incorrect view. :wink: But I can at least explain what I think of when it comes to typeclasses …

If you take a set of classes and a set of common methods that apply to those classes with slightly different implementations then you effectively end up with a two dimensional array, with classes being one axis, and method implementations being the other axis.

I see OO as a mechanism that slices that two dimensional array along the first axis, with the primary grouping by class, where each class/subclass contains the method definitions.

My understanding was that typeclasses are primarily a mechanism to slice that same two dimensional array the other way. I.e. the method are grouped by method implementation across the classes (which might just hold data and no methods).

Of course, it also seems reasonable to allow some methods to be defined/grouped by the classes (OO style), and some methods to be grouped by the method implementation (typeclasses style).

I think that some languages believe OO is sufficient, others believe typeclasses are sufficient. Personally, I suspect that the truth is somewhere in between: for some constructs (e.g. Scala collections library, OO seems to work particularly well), but in other cases (e.g. perhaps JSON transformers) typeclasses seem like a better fit.

So, I see “typeclasses” as being a dual to “OO” rather than equivalent of visitor pattern, or builder pattern, etc.

Finally, it seems to be me that extension methods add extra methods to a single class, but typeclasses are somewhat similar in that they are adding methods to a set of classes rather than to a single class.

1 Like

type class is a programming technique that lets you add new behavior to closed data types without using inheritance, and without having access to the original source code of those types.

I like this definition too, I think it covers two useful usage of typeclasses:

Indeed: adding a new behavior to say, String, which you don’t own, to make it conform to a contract of your own code. In Scala 3, we could have imagined achieving such kind of thing through extension methods (+ override) for example, that’s simply “making String extend XXX”

But it also covers a use-case where type classes really shine (and even in Scala 3): implementing a behaviour between types like the ability for different types to work with each other, or produce a result.

Here we’re no longer extending String, to have it implement an interface, or extending Int to have it implement an interface, it no longer makes sense that Int or String holds the relationship, we’re defining how Int and String should work together to produce a result.

I am in the process of rewriting this: https://github.com/lampepfl/dotty/pull/8147 so I am for sure going to add any simple definition we could agree on on top of the doc!


Note: that’s why I tried to emphasize in the rewrite on the part “typeclasses are traits whose implementation are defined by given instances”. When you think about the second use-case I mentioned above: it makes more sense that there’s no extend keyword. In this case we’re not implementing or extending a trait but “giving” an implementation of the typeclasses for some types.

I don’t honestly believe that’s there’s anything that can be done for the case you describe.

Someone who’s never seen a typeclass before will almost always be baffled when he sees one for the first time, regardless of the keyword. The same goes for most new concepts. I don’t think it has much to do with being smart, but everything with being familiar with the concept. I would like to think I’m pretty smart, but I was still confused when I saw a typeclass for the first time in Haskell in my Declarative Languages class after being used to Java, Python …

In Scala you don’t need a big paradigm shift to understand what’s going on. You only need to learn what implicits / givens are and all the other stuff you already know.

2 Likes

What about typeprocessor? It strikes me that the key quality of type class instances is that they are external to the type. A type class performs a particular process or set of processes. An instance of the type class performs that process or processes with the particular type T. A key insight is that sometimes the type class will process an input of an instance of the Type T, in which case it is an alternative to object orientated class based inheritance. But the process doesn’t have to take the type T as input. The type T can be an output to the process, it can be an input and output to the process, or it can be neither an input or an output.

I’m not sure where the best place to ask this question is. Feel free to ridicule me for misusing this thread.

My question is: in what way is a type class in Scala similar to a metaclass in CLOS?

Sometimes I get the feeling that a type class tries to compile-time emulate a metaclass.

But if a reader see a keyword like “typeclass instance blah” and googles “Scala 3 typeclass” then it should be possible to describe what is being done fairly simply/concisely in say 1-2 pages of text (including examples of how they are defined and used). This is also what I would expect they would be able to do if they come across the “collective extension” syntax.

However, if the only keyword that the reader sees is “given blah” then there is perhaps 20-30 pages of non trivial documentation to wade through and properly understand. Maybe they will then figure out that this is meant to be a typeclass, perhaps not. I just think that it a lot harder to do, even for someone who is smart. The context bound is another curve ball to deal with and understand (or ignore, and hope that it isn’t really important).

Then they may see a method definition where there is some stuff in brackets before the method name (a bit weird), and probably be confused. Then they might think that it is just infix notation (okay - that sort of makes sense), but then in this case this isn’t infix notation because actually it is an extension method, and now the type variables also end up in a different place - they are no longer after the method name. I.e. the structure of the language has become far less regular, and I don’t see that as a good thing.

Perhaps the complexity of these concepts on their own is reasonable/acceptable, but when you put them together I think you end up with something really very complex. I used to know C++ but wouldn’t bother with it today, because the language is just too big and too complex. Similarly, for Rust, I think that it has some great ideas, but my impression is that the borrows checker adds too much unnecessary complexity - garbage collection is probably the better pragmatic choice. Languages like C, Java, Go are the other way round, the language is simple, but then the code ends up potentially being very verbose. But I would estimate that 90% of my professional colleagues would argue that a simpler language is good enough, and better than a complex one.

Scala 2.13 makes writing some code blissfully simple, but there is a underlying complexity that bubbles under the surface. When Martin announced Dotty & Scala 3, I had interpreted his goal of taking Scala 2, keeping the best bits, getting rid of [some] warts, simplifying some of the underlying mechanics, and hopefully allowing me to access some of the more advanced parts of the language that I had previously shied away from due to their complexity.

I think that for individuals who know Scala 2 very well, and have been working closely on the Scala 3 specification, and debating it for a long time, then what can seem obvious and intuitive is not necessarily the case for folks who are less familiar and haven’t been involved in the specification. Perhaps books and education will solve this gap, but my nagging feeling is that Scala 3 could benefit if there was a bit less (exposed) complexity.

1 Like

After some internal disussion, we arrived at a tweak that hopefully combines the best parts of the syntax of the current release and the alternative proposal that’s implemented in master.

A PR that changes the docs is here: https://github.com/lampepfl/dotty/pull/8162. Comments are invited on that PR (rather than here since here we seem to be in more general territory). If there are no major objections, this will be the syntax supported in Dotty 022 to be released next week.

2 Likes

I don’t know enough about metaclasses in CLOS to be able to say for sure. At first glance they seem to have different roles.But maybe you can tell us what similarities you see?