Adaptive methods and constants. No Type Classes #2345
Replies: 17 comments
-
How would this work under the hood? |
Beta Was this translation helpful? Give feedback.
-
It would have to compile Sum for each TArg as each TArg comes with its own implementations |
Beta Was this translation helpful? Give feedback.
-
I'm not sure I understand. Can you show some example code, as well as how that example looks 'desugared' |
Beta Was this translation helpful? Give feedback.
-
under the hood it would compile
for a Vector2 it would use (0,0) and the + operator of Vector2 and compile another Sum_For_Vector2. So when thinking from an entry point (you main program) where all the type parameters have been replaced with concrete type arguments the compiler will in the end have replaced all the adaptive calls with concrete implementations and will call the appropriate methods (like Sum_for_Vector2). the big questionmark is how we would put a |
Beta Was this translation helpful? Give feedback.
-
That sounds like it would work like C++ templates rather than C#/CLR generics. How would that work across assemblies? |
Beta Was this translation helpful? Give feedback.
-
That indeed is a big ?. |
Beta Was this translation helpful? Give feedback.
-
Yes, as of the "shapes" proposal the "typeclass" would be emitted more or less as a normal generic interface where all of the members would be converted into suitable instance members and that would be exposed from the assembly. The compiler would recognize that interface as a "typeclass" presumably using some additional metadata like an attribute. The compiler would then generate witness structs implementing that "typeclass" interface to wire up the target members.
Both would involve embedding a lot of voodoo into the assembly. Much of F#'s typesystem doesn't work outside of F# for this reason. In the case of embedded source, how would a compiler from any other language be able to interop? With "typeclasses" these issues don't exist. |
Beta Was this translation helpful? Give feedback.
-
Ok, wait, this should be doable...
But let's try it from scratch.
|
Beta Was this translation helpful? Give feedback.
-
Having the C# compiler emit specialized versions of the method would be a pretty big departure from the current generic paradigm and the team has not expressed interest in implementing such a feature. If the compiler has to emit a different version of the method for every possible combination that will result in a lot of assembler bloat. Not to mention, I think that the declaration, use and consumption of these types of features will be mostly between separate assemblies.
For what I consider to be the majority case that would be quite a bit more complicated and cumbersome while also being a lot less efficient than the interface/witness approach, where a constrained interface call guarantees both specialization and inlining. You don't get more efficient than that. I personally don't see the benefit of having these "adaptive methods" separate from typeclasses. Grouping them under a nominal structure makes it easier to group them for common operations and it fits in a lot easier with the language and the runtime. You're still free to declare each typeclass with a single method. |
Beta Was this translation helpful? Give feedback.
-
Well, this is optional. In our examples, it's often been about computing with "number types" and it felt like a bad idea to do this via an interface method calls. I think several compilation strategies are possible. It would also be interesting what could be done with expression trees. But then there are still the interface or delegate way left, which is the easiest approach that also doesn't arise any issues between assemblies. That said: the most important aspect of the proposal is the ease of use.
I am not sure if I can follow. In here I see regular interface calls. Back to the adaptive idea: Wouldn't we get roughly the same performance with the delegate or interface compilation strategy?
The main point is simplicity. |
Beta Was this translation helpful? Give feedback.
-
I am basing the discussion on the "shapes" proposal which is probably the more of the fleshed out variations discussed on this repository. This is certainly very early in discussion, though, and there have been other alternative proposals put forth. In this proposal the interface call through to the "witness" struct is constrained and the generic type parameter for the witness of the method making the call is further constrained to be a
The same is true of the proposal I mentioned. While the typeclass does compile down to an interface you are never expected to actually implement it. When you call a method that expects the typeclass the compiler will determine if the type is compatible and will emit a witness struct which will handle wiring up the required members. You're not required to do anything else. Here's another more recent discussion which explores the idea of "extension interfaces" instead, which are similar to "shapes"/"typeclasses" except building on the idea of "extension everywhere" which aims to allow developers to add arbitrary members to existing types, not just methods. I expect that after C# 8.0 is released that this topic will explode and there will be a lot of discussions and spaghetti thrown at the wall. But in my opinion some of the critiques I have of this proposal will be important to whatever the team eventually decides to build, in that whatever it is has to have a good story for consumption across assembly boundaries without requiring very language/compiler-specific metadata (like embedded source) and not adding overhead at runtime. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour: thanks for all the good input so far! Regarding the optimization. Got it. It's a difference if you hand over the interface or a type that is constrained to the interface. Missed that bit. Just a small comment to this adaptive proposal:
could translate to
interesting |
Beta Was this translation helpful? Give feedback.
-
That would pollute the namespace, way more than typeclasses. Since they're static and 'open' (like extension methods, they don't need full names to access them, but they don't rely on the operand type to narrow down the search). And you have to mention every single function you want to use on a given type. So it seems like the proposal's core is actually the generic constraint on method name, not the adaptive methods part. Like: public static class IntExtensions { // no extension everything
public static int Add(this int x, int y) => x + y;
public static int Zero(this int _) => 0; // because no extension-everything yet
}
public static class Program {
public static T Sum<T>(T[] values)
where T : .Add(T), .Zero // method constraints
{
T acc = T().Zero(); // because no extension-everthing yet
// (here missing: static methods, properties)
foreach (var value in values)
acc = acc.Add(value); // missing static methods
return acc;
}
void main()
{
Sum(new int[] { 1, 2, 3, 4, 5 }); // 15
}
} But that doesn't say how such generic code is supposed to look for and choose the right extension methods. Are they all passed in a dictionary from the calling context? Maybe a static class is implicitly defined for every type used in a file that gathers all of its extension methods, and that is passed to any generic method called that has a method-existence constraint? And that brings us right around to coherence issues. Which justifies the It feels like adaptive methods would be to typeclasses what free functions would be to the language. |
Beta Was this translation helpful? Give feedback.
-
@narfanar: So maybe I should be honest about that I wasn't really aware of all the work that went into the above-mentioned proposals. It will take me some time to catch up. And I am not sure if I actually already want to go and compete against concepts, shapes, roles, static interface members, extension-everywhere and Haskell type classes etc. at the same time :D! My proposal comes from a different background: adaptive nodes in VL and type classes in Haskell. And I do want to see how it would apply to C# and what it could bring to the table. Your points: Not Extension-everywhere: About pollution:
would maybe be more special and hide somewhere else or it wouldn't even be predefined. You would need to define it yourself.
where I specialize the adaptive operators and the adaptive But on application side, I want to provide implementations. So what about application side?
I wasn't aware up to now of the
which would now make the static members only visible for when it's about finding implementations for an adaptive method argument on the calling site
or similar. I don't want to push this too far as I actually think that all the other proposals definitely have A LOT to offer and seeing all the hard work to get each small detail right and make different ideas to work together is just impressive. It's also very good to see a community trying to work all this out together. I am just curious what this rather small grained feature can offer over type classes. I don't know. Maybe you would argue: This combination of members is somewhat special. So define your own type class. If possible I would maybe do so and don't use any of the other number type classes, even though there are already plenty for every little added members there is just another one. Haskell number type classes also "inherit" from each other. So the user gets the feeling: these you need to understand and to know of and to use. Don't even go near defining your own concept. Even though that is the real power: To be able to define the smallest possible shape for already existing functionality, make it match if it doesn't already and when adding new types only having to offer the actually needed functionality to apply to that shape. I can imagine warnings on unused adaptive nodes. So intellisense offers me to kick them out of my signature. I can also imagine an intellisense feature that helps me adding missing "using" statements. (In our visual language VL you don't need to specify them as the body of the method gets looked at before finishing the signature, but I guess something like that is a no go; isn't an operator already kind of "adaptive" in C#: you only can specify which one to use via the used types. It just happens to not work over method borders and type parameters.) It would be interesting for what other cases these little fellows can be used. Can I call |
Beta Was this translation helpful? Give feedback.
-
we can turn things around and ask for some sugar for this console app written in C#7.0:
how would that look like with the different approaches? |
Beta Was this translation helpful? Give feedback.
-
maybe there is a more performant way to compute the cubic bezier for floats and 2d vectors using polynomials? Then I would again abstract over CubicBezier via another adaptive definition so that the generic implementation is just one of the possible implementations of the now adaptive CubicBezier. Maybe we could even show warnings when someone calls the generic Lerp (handing over 4 members) instead of the adaptive implicitly defined adaptive Lerp (that not only simplifies the set of constraints quite a bit but also opens the door for new ways to plug into the system. Dear all, sorry, I know this goes into a whole other direction than everything that you were up to. So please take it as a hopefully refreshing and somewhat different idea from a parallel universe. |
Beta Was this translation helpful? Give feedback.
-
Could that work:
The compiler could try different approaches to get its enumerator:
On caller site you would be able to hand over a static (extension) method or an instance method or getter. The compiler would generate a static method for you that takes the instance, calls the getter and returns the enumerator... Is it a good style to use such a using statement? Probably not. I think the question has been answered somewhere already: the user doesn't want to have different ways of specifying that she/he needs an enumerable thing. We want to accept an argument of type But: how to get one on the fly on the calling site if the original type doesn't offer one? That's totally unrelated, but here is another idea: instead of attaching interfaces via an extension with implicit (identity) conversion, we could steal from other more explicit ideas like https://fsharpforfunandprofit.com/posts/object-expressions/. In C# this could read like this:
which would extend the anonymous type feature, with see-through capabilities and the ability to implement interfaces. It would also implement all the interfaces of the compile time type of myObject. As it is the anonymous type feature it would also allow to add more state. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hello!
regarding haskell type classes: I once admired them, but then again: this is soo hard. How often would you come up with a type class and also get it right. Would they inherit from each other and how to name them properly. Look at all the type classes in haskell that deal with numbers only.
https://github.com/MattWindsor91/roslyn shows how type classes could look like in C#.
Here is another idea: Don't bundle methods and name the concept. Just focus on the members...
Introducing adaptive methods:
Comparison
Type classes bundle methods and give a name to the concept. Adaptive methods are lonesome cowboys. No burden to come up with "the right" type class design and give it "the right" name. Just use the methods you need and communicate via the signature that you long for some implementation for these methods.
Step 2: Cleanup mainly. Adding adaptive Constants and operators
Replace Add with the + operator. Zero with 0. The adaptive definitions for them and the implementations for basic data types would already get shipped with the BCL. With that in mind you get:
The proposed design works fine for the visual language VL that is also targeting .Net. We call it adaptive nodes.
@MattWindsor91
What do you think?
Beta Was this translation helpful? Give feedback.
All reactions