Champion "Type Classes (aka Concepts, Structural Generic Constraints)" #8878
Replies: 189 comments
-
/cc @MattWindsor91 |
Beta Was this translation helpful? Give feedback.
-
Please consider type classes on generic types as well. |
Beta Was this translation helpful? Give feedback.
-
@orthoxerox The proposal supports type classes on generic types. Unless perhaps I don't understand what you mean. |
Beta Was this translation helpful? Give feedback.
-
@gafter the proposal might have evolved since I last read it, but I remember that a monad concept could be implemented only in a very convoluted way, the signature of SelectMany with a projector had like 10 generic parameters. |
Beta Was this translation helpful? Give feedback.
-
@orthoxerox It does not support higher-order generics. |
Beta Was this translation helpful? Give feedback.
-
That's what I meant. |
Beta Was this translation helpful? Give feedback.
-
@gafter Is there any chance that higher-order generics could be considered? I was going to stay out of this conversation for a while, but I have managed to get most of the way there by using interfaces as type-classes, structs as class-instances (as with Matt Windsor's prototypes), and then using constraints to enforce relationships: Along the way I have had to make a number of compromises as I'm sure you would expect. But the majority of the 'higher order generics' story can be achieved with a significantly improved constraints story I feel. And with some syntax improvements that give the appearance of higher-order generics, but behind the scenes rewritten to use constraints. For example I have a Monad type-class public interface Monad<MA, A>
{
MB Bind<MONADB, MB, B>(MA ma, Func<A, MB> f) where MONADB : struct, Monad<MB, B>;
MA Return(A x);
MA Fail(Exception err = null);
} Then a Option 'class instance' public struct MOption<A> : Monad<Option<A>, A>
{
public MB Bind<MONADB, MB, B>(Option<A> ma, Func<A, MB> f) where MONADB : struct, Monad<MB, B>
{
if (f == null) throw new ArgumentNullException(nameof(f));
return ma.IsSome && f != null
? f(ma.Value)
: default(MONADB).Fail(ValueIsNoneException.Default);
}
public Option<A> Fail(Exception err = null) =>
Option<A>.None;
public Option<A> Return(A x) =>
isnull(x)
? Option<A>.None
: new Option<A>(new SomeValue<A>(x));
} The big problem here is that for The functor story is an interesting one: public interface Functor<FA, FB, A, B>
{
FB Map(FA ma, Func<A, B> f);
} With an example public struct FOption<A, B> : Functor<Option<A>, Option<B>, A, B>
{
public Option<B> Map(Option<A> ma, Func<A, B> f) =>
ma.IsSome && f != null
? Optional(f(ma.Value))
: None;
} Notice how I've had to encode the source and destination into the interface. And that's because this isn't possible: public interface Functor<FA, A>
{
FB Map<FB, B>(FA ma, Func< A, B> f) where FB == FA except A is B;
} If we could specify: public interface Functor<F<A>>
{
F<B> Map<F<B>>(F<A> ma, Func<A, B> f);
} And the compiler 'auto-expand out' the As @orthoxerox mentioned, the generics story gets pretty awful pretty quickly. Here's a totally generic public static MD Join<EQ, MONADA, MONADB, MONADD, MA, MB, MD, A, B, C, D>(
MA self,
MB inner,
Func<A, C> outerKeyMap,
Func<B, C> innerKeyMap,
Func<A, B, D> project)
where EQ : struct, Eq<C>
where MONADA : struct, Monad<MA, A>
where MONADB : struct, Monad<MB, B>
where MONADD : struct, Monad<MD, D> =>
default(MONADA).Bind<MONADD, MD, D>(self, x =>
default(MONADB).Bind<MONADD, MD, D>(inner, y =>
default(EQ).Equals(outerKeyMap(x), innerKeyMap(y))
? default(MONADD).Return(project(x,y))
: default(MONADD).Fail()));
public static MC SelectMany<MONADA, MONADB, MONADC, MA, MB, MC, A, B, C>(
MA self,
Func<A, MB> bind,
Func<A, B, C> project)
where MONADA : struct, Monad<MA, A>
where MONADB : struct, Monad<MB, B>
where MONADC : struct, Monad<MC, C> =>
default(MONADA).Bind<MONADC, MC, C>( self, t =>
default(MONADB).Bind<MONADC, MC, C>( bind(t), u =>
default(MONADC).Return(project(t, u)))); A couple of issues there are:
Obviously all of this is of limited use to consumers of my library, but what I have started doing is re-implementing the manual overrides of things like public Option<C> SelectMany<B, C>(
Func<A, Option<B>> bind,
Func<A, B, C> project) =>
SelectMany<MOption<A>, MOption<B>, MOption<C>, Option<A>, Option<B>, Option<C>, A, B, C>(this, bind, project); So my wishlists would be (if higher-order generics, or similar are not available):
Apologies if this is out-of-scope, I just felt some feedback from the 'front line' would be helpful here. And just to be clear, this all works, and I'm using it various projects. It's just boilerplate hell in places, and some hacks have had to be added (a |
Beta Was this translation helpful? Give feedback.
-
@louthy Without thinking too deeply about it, I would ask the questions
|
Beta Was this translation helpful? Give feedback.
-
This may lead to a totally new (and separate?) standard library, with this as the base. |
Beta Was this translation helpful? Give feedback.
-
My understanding was that higher-kinded types would need CLR changes, hence why Claudio and I didn't propose them (our design specifically avoids CLR changes). I could be wrong though. |
Beta Was this translation helpful? Give feedback.
-
Related, since you can only express a subset of type classes without them (Show, Read, Ord, Num and friends, Eq, Bounded). Functor, Applicative, Monad and the rest require HKTs.
Yes. Unless there's some clever trick, but then they won't be CLS compliant.
Not that straightforward. The design choices are more or less clear.
As much as any other type classes would. |
Beta Was this translation helpful? Give feedback.
-
@orthoxerox has concisely covered the points, so I'll try not to repeat too much.
I think this was always the understanding. Obviously the 'hack' that I've done of injecting the inner and outer type (i.e. public struct MOptTry<A, B> : Functor<Option<A>, Try<B>, A, B>
{
public Try<B> Map(Option<A> ma, Func<A, B> f) => ...
} Which obviously breaks the functor laws. It would be nice to lock that down. If it were possible for the compiler to expand So, I'm not 100% sure a CLR change would be needed. The current system works cross assembly boundaries, so that's all good. The main thing would be to carry the constraints with the type (is that CLR? or compiler?). If adding a new more powerful constraints system means updating the CLR, is it better to add support for higher-order types proper?
I've gotten a little too close to using them with C# as-is. But I suspect looking at Scala's higher-order types would give guidance here. I'm happy to spend some time thinking about how this could work with Roslyn, but I would be starting from scratch. Still, if this is going to be seriously considered, I will happily spend day and night on this because I believe very strongly that this is the best story for generic programming out there.
I believe so, but obviously after spending a reasonable amount of time working on my library, I'm biased. The concepts proposal is great, and I'd definitely like to see that pushed forwards. But the real benefits. for me, come with the higher-order types. |
Beta Was this translation helpful? Give feedback.
-
Given that changes that require CLR changes are much, much more expensive to roll out and require a much longer timeframe, I would not mix higher-kinded types into this issue. If you want higher-kinded types, that would have to be a separate proposal. |
Beta Was this translation helpful? Give feedback.
-
@gafter Sure. Out of interest, does 'improved constraints' fall into CLR or compiler? I assume CLR because it needs to work cross-assembly. And would you consider an improved constraints system to be a less risky change to the CLR than HKTs? (it feels like that would be the case). I'm happy to flesh out a couple of proposals. |
Beta Was this translation helpful? Give feedback.
-
If the constraints are already expressible (supported) in current CLRs, then enhancing C# constraints to expose that is a language request (otherwise a CLR and language request). I don't know which is easier, constraints or HKTs. |
Beta Was this translation helpful? Give feedback.
-
The idea of the functor and the monad require higher kinded polymorphism, even more so than it requires the concept of type classes. In fact, in a language and runtime such as C# and .NET, should higher kinded polymorphism be made available (most likely as a form of generic constraint), the concept of functors and monads could be implemented as an interface instead of a type class. Really, the way I was most able to understand the concept of a type class is that it is similar to the idea of an interface which can be externally applied to a type, similar to how extension methods are methods which can be externally applied to a type. |
Beta Was this translation helpful? Give feedback.
-
In languages like Kotlin, that do not support Higher Kinded Polymorphism, they have adopted a pattern that specifies the kind separately see https://arrow-kt.io/docs/0.10/patterns/glossary/#higher-kinds . Yet type classes are still useful |
Beta Was this translation helpful? Give feedback.
-
I enjoyed the whole discussion on this thread. Thanks all! I hope C# will eventually have nice solution for higher-kinded types. I'd like to share my experiments to encode HKT with generics. My interest is how we can use HKT and escape know issues like difficult composition of monad transformers or free monads which requires extra structure with impact on performance. If we see generics as ability to have type level variables, higher-kinded type is ability to define a function on the type level. Of course in a limited way like in Scala, not "the same" function like in Idris. |
Beta Was this translation helpful? Give feedback.
-
I think HKT and Type Classes should be considered together. |
Beta Was this translation helpful? Give feedback.
-
They are completely unrelated. For example, Rust has type classes but doesn't have HKT, and there is no reason why the opposite configuration cannot exist (although I don't recall any example). Thus there is no reason to "consider them together" |
Beta Was this translation helpful? Give feedback.
-
There's one possible reason: leaving places in syntax tokens and BCL for "once it's a thing", to avoid future breaking and ecosystem inconsistence. |
Beta Was this translation helpful? Give feedback.
-
Has there been any progress on this issue? I'm not quite sure why concepts/shapes/whatever would be distinct in the language from interfaces. It seems to me that they both serve the same purpose, to define contracts. Is the separation just an ugly manifestation of the underlying implementation details, or am I missing something? |
Beta Was this translation helpful? Give feedback.
-
@chkn there is seperate proposal to extend interfaces themselves, type roles in search and that proposal should pop up. As for different implementations this is because shapes and the like wouldnt require changes to the runtime which is huge plus for language team. Extending interfaces require changes to the runtime which is considered highly undesirable but not showstopper though |
Beta Was this translation helpful? Give feedback.
-
This was highly undesirable before core/5. It is not now. Personally, I believe interfaces should indeed be turned into type classes. |
Beta Was this translation helpful? Give feedback.
-
Just to concur here, really: a lot of the specifics about the version of concepts Claudio and I prototyped were explicitly to avoid runtime changes per the situation with the CLR circa 2016. Now that the CLR is less immutable, it makes sense to do whatever makes concepts a good citizen in the C# ecosystem and with existing code and practices, and having them stand entirely separately from and parallel to interfaces is an issue I remember coming up repeatedly. One of the nice things about the concepts prototype was the degree of static resolution/devirtualisation/inlining that the CLR could do when it applied concept methods on known types - it'd be nice from a perf point of view to have something like this, but it's arguably a very different world to normal interfaces. (Apologies, I've been mostly out of the loop on this for a few years now, so this might've already been a point of discussion!) |
Beta Was this translation helpful? Give feedback.
-
What changed? |
Beta Was this translation helpful? Give feedback.
-
We no longer ship major version updates same day to billions of devices. |
Beta Was this translation helpful? Give feedback.
-
Thanks! I found #1711, which is more what I was imagining.
Yeah it's a good proposal, and using structs as witnesses is a brilliant idea. Definitely the nicest thing if runtime support is out of the question. I also believe the same optimizations can occur for generic types constrained to interfaces. |
Beta Was this translation helpful? Give feedback.
-
does this topic cover restraining generics to operators? |
Beta Was this translation helpful? Give feedback.
-
See
Beta Was this translation helpful? Give feedback.
All reactions