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.
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:
- 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 animplicit 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 implicit
s 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).
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).
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.
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
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.
What about alexknvl more conservative syntax proposition?
I know that it is not exhaustive proposition but maybe it can be polished?
Scala has type members. I still don’t get why those multi-parameter typeclasses don’t use them.
trait Collection[C] { // or whichever syntax you prefer
type E
}
trait MonadError[F[_]] {
type E
}
I’m unsure about your wording, since your wording implies you talk about individual call sites, but in general guaranteeing coherence is not possible, especially due to dynamic linking. It’s very hard to guarantee that an Eq[A]
wasn’t defined multiple times in any of the project’s dependencies and having such a guarantee in the project’s own source files is a pretty weak guarantee to have.
IMO a warning is enough. Coherence is mostly achieved by convention and that’s OK. Even in Haskell sometimes you just need orphans (e.g. JSON serialization).
Scala has type members. I still don’t get why those multi-parameter typeclasses don’t use them.
Because it doesn’t work. E.g. a type member doesn’t work for MonadError
since obviously you can have multiple MonadError
instances defined for each (F[_], E)
tuple you care about and its behavior can change depending on E
. Not many such instances in the wild, but type members and type parameters are apples vs oranges.
And the E
type is also exposed in the function signatures, which means you lose type information about it when working with the generic MonadError[F]
interface. You can only materialize E
when the compiler knows the exact instance you’re using, otherwise the type is going to be MonadError[F]#E
, which is pretty useless and annoying.
I’d like to point out that … if we are going to introduce type classes, it’s very important to look at actual usage in the wild. We need multiple type parameter classes, because multiple type parameter classes have been in use for a long time. In Cats-Effect too we have been discussing multi-param type classes in order to accommodate “bifunctor IO” implementations.
Yes, there’s an entire ecosystem that uses type classes, see the projects at Typelevel.org.
Type classes as a concept are equivalent with implicit parameters (which is why Scala is an awesome language for FP) and, in comparison with OOP subtyping, type class restrictions play better with generic type parameters and static typing.
As Luka is explaining, nowadays we have to do a lot of boilerplate in order to encode these concepts. And we can use macros in order to reduce that boilerplate (see Simulacrum), but AFAIK such libraries will no longer be possible on top of Scala 3 / Dotty due to no longer having such macros capabilities.
Book authors like the authors of Scala with Cats have complained that beginners have a really hard time grasping FP concepts due to type classes being so hard to understand due to the extreme boilerplate and ceremony involved.
Scala is a language that has thrived on its HKTs and ability to define and use type classes. That’s one of the features that make Scala shine in comparison with other languages. It’s time to make that official.
And it would be a shame if Kotlin got type classes first.
But you cannot have MonadError
instances for any (F[_], E)
tuple. For IO
it’ll always be Throwable
. For IO[E, ?]
it’ll always be E
. For Either[E, ?]
it’ll always be E
. And ok, for EitherT[IO, E, ?]
I guess it’ll be one of Throwable
or E
. But it’s still determined by F
. For EitherT
you’ll just have to give priority to one of both instances, and use an orphan instance if you want to use the other, kind of like when you want a *
Monoid instead of the default +
.
That’s exactly my point, actually. If one type is determined by the other, I think that a type member is a better fit.
Currently you can use the Aux
pattern when you need to be able to know inside your method that e.g. E =:= String
. If we’re scrapping boilerplate for typeclasses, you could consider boilerplate free Aux
as well.
@Jasper-M You proposal breaks for polymorphic code. Consider this:
def foo[F[_]](implicit F: MonadError[F, Throwable]): F[String] =
F.raiseError(new Exception("problem")).handleError {
case e => e.getMessage
}
val a = foo[IO]
if you used a type member for MonadError
, we’d have:
def foo[F[_]](implicit F: MonadError[F]): F[String] =
F.raiseError(new Exception("problem")).handleError {
case e => e.getMessage // Doesn't compile
}
because we lose info about which E
we are talking about. To recover that you need the Aux pattern
object MonadError {
type Aux[F, Err] = MonadError[F] { type E = Err }
}
so that we can implicit F: MonadError.Aux[F, E]
, at which point it’s just better to use two type parameters instead. There are multi param type classes that benefit from type members (cats Parallel for example), but MonadError isn’t one of them imo: it only makes sense to hide E
when you are dealing with a concrete type, but in that case you don’t need a typeclass constraint at all, and when you do, you’d have to use Aux
anyway, so it really makes more sense to have two type params there.
Note that the example you made in your comment here, i.e.
def bar[F[_]: MonadError](f: F[String]) = ???
Doesn’t really work: polymorphic code that calls bar
cannot recover from the error, because it has no information about E
unless: F
is instantiated to a concrete type (you lose polymorphism), or bar
is changed to use Aux
(no benefit from having a type member)
Furthermore, there are still cases where a multi param type class still needs two or more type params, even with type members, for example:
class Mult a b c | a b -> c where
(*) :: a -> b -> c
instance Mult Matrix Matrix Matrix where
{- ... -}
instance Mult Matrix Vector Vector where
Here c
can be a type member, but a
and b
make sense as type parameters. Shapeless provides countless real-world examples of this
type class
would harmonize withcase class
if not a problem with different inheritance notation. New keyword is also pretty big deal.
If adding a new keyword represents such a big challenge I completely understand. I just wanted to point out how confusing it might sound for newcomers:
type
: to define type aliases. Egtype Username = String
class
: to define and instantiate classes. Eg.class Foo()
andval foo = new Foo()
type class
: to define a type classes. Eg.type class Monoid[A] : Semigroup[A] {}
Is there such big need for defining typeclasses?
Yes. As @alexandru pointed out there’s an entire ecosystem making use of them and we would benefit from having an easier way to work with them.
Creating instances could be more important but still we should try to keep syntax as close current scala as possible
Changes are already being made (opaque types, type lambdas, etc) and I think it’s for the better. Keeping syntax as close as current Scala is secondary IMHO.
What about alexknvl more conservative syntax proposition?
His second (more conservative) option looks pretty similar to what we can do today in Scala 2 and I can’t see how type classes syntax could be defined. However, I believe we can do better for Scala 3 by having native support for type classes.
BTW I think it’s great Alex is managing to get that to work in Scala 2 with the scalafix plugin!