-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Champion "Type Classes (aka Concepts, Structural Generic Constraints)" #110
Comments
/cc @MattWindsor91 |
Please consider type classes on generic types as well. |
@orthoxerox The proposal supports type classes on generic types. Unless perhaps I don't understand what you mean. |
@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. |
@orthoxerox It does not support higher-order generics. |
That's what I meant. |
@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 |
@louthy Without thinking too deeply about it, I would ask the questions
|
This may lead to a totally new (and separate?) standard library, with this as the base. |
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. |
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. |
@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. |
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. |
@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. |
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. |
@gafter Does this concept proposal also support anonymous concept? Such as public void SetPosition<T>(T pos) where T : concept{ float X,Y,Z; }
{
x = pos.X;
y = pos.Y;
z = pos.Z;
} |
@Thaina You can read it for yourself; I believe the answer is "no". What translation strategy would you recommend for that? |
@gafter Direct answer like that is what I just need. I can't understand that is it yes or no. I don't really even sure that your comment was directed at me
I still don't understand what |
@Thaina the answer is no, it doesn't. Open a separate issue if you want anonymous concepts. |
@orthoxerox That's what I want. Actually I try to ask because I don't want to create duplicate issue. So I want to make sure that I could post new |
(I thought I'd replied to this, but seemingly not… maybe I forgot to hit comment!) @Thaina I'm not entirely sure I understand the anonymous concept syntax you propose, because it seems to be selecting on object-level fields/properties instead of type-level functions (Claudio's/my proposal only does the latter). This seems more like it'd be an anonymous interface? Interop between concepts and 'normal' interfaces is another unresolved issue with our proposal. Ideally there should be a better connection between the two. The main distinction is that interfaces specify methods on objects, whereas concepts specify methods on an intermediate 'instance' structure: concepts can capture functions that don't map directly onto existing objects, but are ergonomically awkward for those that do. |
@MattWindsor91 I was really misunderstanding thanks for your clarification |
Doesn't that make it a better example? It's already sort of doing something we're expecting shapes to allow. |
I'm wondering why the examples (https://github.com/MattWindsor91/roslyn/blob/master/concepts/code/ConceptLibrary/Prelude.cs) are missing popular concepts like functor and monad? Is the lack of higher-kinded polymorphism the obstacle here? If that's not the case then I'm just not smart enough to figure out their definition. Could someone please help and show me how a functor would be defined as a |
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. |
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 |
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. |
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" |
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. |
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? |
@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 |
This was highly undesirable before core/5. It is not now. Personally, I believe interfaces should indeed be turned into type classes. |
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!) |
What changed? |
We no longer ship major version updates same day to billions of devices. |
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. |
does this topic cover restraining generics to operators? |
See
The text was updated successfully, but these errors were encountered: