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.
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.
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.
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?)
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.
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:
Guarantee that whenever someone implicitly summons a typeclass, it’s always the same instance (coherence).
Being able to do (e: Eq[A]).contramap when declaring implicit instances.
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.
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])
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.
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
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.
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
Here are some thoughts I have after re-reading the proposal:
Extension methods
Introducing a new extension keyword sounds good to me
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
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.
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.