Dotty Type classes

I like this proposal a lot! It’s a great distillation of the many excellent ideas proposed in previous discussions.

It has a lot of advantages:

  • The mechanisms involved will be familiar to Kotlin and Rust programmers
  • It’s obvious which methods are eligible candidates for extension
  • It only uses one new keyword
  • It (mostly) reuses the existing syntax for context bounds

I’m going to echo @LukaJCB and urge the thread to avoid getting bogged down in discussion of the syntax :slight_smile:

2 Likes

Question about extension methods-
The example from your proposal:

object Ops {
  extension def **(this i: Int)(j: Int): Int = Math.pow(i, j)
}

Can’t we just use the implicit keyword instead of extension, since we have the new way of using this?

object Ops {
  implicit def **(this i: Int)(j: Int): Int = Math.pow(i, j)
}

I feel it’s more consistent with:

trait Foo
object Foo {
  implicit def fooToInt(f : Foo) : Int = ???
}

Sure, I’m definitely open to that, though as I said earlier, I think we should focus on the features and semantics before talking about syntax :slight_smile:

1 Like

Sorry, but its one of my bug bears. You use Monoid in the example, but Monoid is not a typeclass, its an operation class, at least in Scala. Multiplication and Addition are both Monoid operations, on Integers, I’m sure if one delved into it one could find more monoid operations for Integer.

Hi @RichType,

Monoid is a type class because any set of functions can be a type class and in Typelevel Cats we decided to have it, see Monoid. And there’s no such thing as an “operation class”.

You have a point that multiplication and addition can both be used to define monoids for integers, however you can also define a multiplication monoid if that’s what you want, you just have to define an opaque alias for Int (aka a “newtype”) and the coherence rule will still hold.

The proposal is not about particular type classes to be added to the standard library though, Monoid was used because it’s a very simple example that drives the point accross.

5 Likes

I would like to add that I think it would be very very nice to support what Rust calls “trait objects” which are basically the typeclass version of dynamic dispatch. That is, I think we should have the ability to deal with collections of data which all have a typeclass instance, where each element may have a different underlying typeclass instance (rather than all elements having the same typeclass instance). See this discussion for further motivation.

3 Likes

yes there are more: min, max, bitwise xor, and, or.

That said, non coherent typeclasses don’t seem to disqualify them in scala (is Ordering a typeclass? what about Ordering.by, how about the fact there are two good choices for Applicative on List?)

To me, dealing with coherence can be done with the opaque types proposal: https://docs.scala-lang.org/sips/opaque-types.html

3 Likes

So I guess the identity value for min is 2,147,483,647.

So while I’ve long wished that Functor and Monad were part of the standard library, but been sceptical of the value of Monoid and Semi Group instances. So I thought I’d see what Cats had to say.

val l = List(1, 2, 3, 4, 5)
l.foldMap(i ⇒ (i, i.toString))
//(15,12345)

Having a default combine operator for String is an even worse idea than for Int. Composing semigroups and monoids seems like a bad idea, because the more monoid instances you compose over the less chance that you actually want all the defaults.

In practice the default instances end up being what you usually want. It’s a pragmatic decision but as a longtime user of this stuff I think it’s the right call. I very seldom want a different one, and when I do I use a newtype and it’s fine.

Note that in some cases like Boolean there’s no obvious way to pick a canonical instance so we just don’t have one.

3 Likes

Just to emphasise the point I’ve changed the values:

val l = List(81, 2, 12, -15, 0, 76, -3)
val l2 = l.foldMap(i ⇒ (i, i.toString))
println(l2) 
//(153,81212-15076-3)

For instances to be coherent they have to be placed either in the companion object of the type class or the companion object of the data type the instance is defined for. We should still allow local instances that can be brought in scope like they can today, but the compiler should at least emit a warning.

This is not enough for coherence. You also need to have an overlapping instance checker.

Here is what I want to see from the orphan checker:

  1. Guarantee that whenever someone implicitly summons a typeclass, it’s always the same instance (coherence).
  2. Being able to do (e: Eq[A]).contramap when declaring implicit instances.
  3. Being able to operate with an a: Eq[A] as if it’s just a record as long as you don’t export it implicitly.
  4. Being able to unsafely turn a: Eq[A] into an implicit val a : Eq[A].

This is considerably better than what Haskell does, because typeclasses become first-class values (for the most part).

It’s sort of TcEq[A] <: Eq[A], where implicits must have type TcEq[A]. Upcasts are free, downcasts are only visible in the companions. You can (a : Eq[A]) : TcEq[A] @orphan (or something to that effect) to unsafely downcast. (TcEq[A] and Eq[A] should be visible to the user as the same type, but from the POV of the typechecker, all implicit val eq: Eq[A] are actually typed as TcEq[A])

Being able to unsafely turn a dictionary into a typeclass gives you reflection / local instances a la reflection/examples/Monoid.hs at master · ekmett/reflection · GitHub (ideally you also need polymorphic values for Rank-2 polymorphism, but we can do without).

5 Likes
type class Semigroup[A] {
  extension def combine(this x: A)(y: A): A
}

type class Monoid[A] : Semigroup[A] {
  def empty: A
}

If a more conservative syntax is used, we might be able to do this with

trait Semigroup[A] extends Typeclass {
  def combine(@this x: A)(y: A): A
}

trait Monoid[A: Semigroup] extends Typeclass {
  def empty: A
}

in Scala2 with scalaz-plugin (https://github.com/scalaz/scalaz-plugin).

5 Likes

I think there shouldn’t be a Monoid for min. Just a semigroup. If you want a monoid, wrap your type in Option. Also, what’s wrong with append as the concat operation for String?

I like the idea of coherent typeclasses and using opaque types for custom instances.

I don’t think it’s fruitful to discuss what exact typeclasses and which variants of them to implement in the Standard Library, at least not in this thread. We should focus exclusively on the feature set of typeclasses in general, to make them easier to define and use in all cases.

7 Likes

Min is a monoid if your type is upper bounded (such as Int.MaxValue, Long.MaxValue, etc…)

It’s actually a BoundedMeetSemilattice: https://github.com/typelevel/algebra/blob/master/core/src/main/scala/algebra/lattice/BoundedMeetSemilattice.scala

Right, now I see there’s even instance (Ord a, Bounded a) => Monoid (Min a) in GHC. My original reasoning was that having a Min for integers in general doesn’t make sense because there’s no representation of integer infinity. Somehow Int.MaxValue still doesn’t sound exactly perfect to me, but I guess it’s the best thing we have :wink:

whether or not Monoids should have a TC representation or not (an interesting topic) is totally orthogonal to TC support in Dotty, and is derailing the discussion of it.

10 Likes

First of all thanks a lot for taking the lead on this proposal @LukaJCB ! I think it is very possible to debate such an important topic in a friendly way let’s keep it up :slight_smile:

Here are some thoughts I have after re-reading the proposal:

Extension methods

Introducing a new extension keyword sounds good to me :+1:

Being able to define extension methods directly in a type class definition sounds great!

Type class definitions

I would rather introduce a typeclass keyword instead of combining the two existing ones type and class because I think that’s confusing for people learning the language. Would this represent a big challenge?

I very much agree that type classes definitions should not permit inheritance. This will tremendously help with coherence.

Not sure having override extension is a good idea but I agree it is sometimes necessary to override default implementations (eg. flatMap) for performance and stack safety reasons but right now I can’t think of any better idea.

Instance declarations

I would rather introduce an instance keyword for this. I find the use of the extension keyword as the valid way to declare type classes instances very confusing since it’s also used to declare extension methods (AKA syntax).

Type class usage

LGTM :+1:

Coherence and multi-param type classes

Overall I think we need more details and examples on this part. AFAIU I think it should be fine if the compiler can determine the type class instance by following the associated types as proposed.

1 Like

type class would harmonize with case class if not a problem with different inheritance notation. New keyword is also pretty big deal.

as i know extension keyword is planned anyway. instance would be new one.

I’m not big fan of introducing such big chunk of syntax for things that can be encoded with other tools, and are not used super widely. Is there such big need for defining typeclasses?
Creating instances could be more important but still we should try to keep syntax as close current scala as possible.

What about alexknvl more conservative syntax proposition?
I know that it is not exhaustive proposition but maybe it can be polished?