dotnet / csharplang

The official repo for the design of the C# programming language
11.58k stars 1.03k forks source link

Champion "Type Classes (aka Concepts, Structural Generic Constraints)" #110

Open gafter opened 7 years ago

gafter commented 7 years ago

See

gafter commented 7 years ago

/cc @MattWindsor91

orthoxerox commented 7 years ago

Please consider type classes on generic types as well.

gafter commented 7 years ago

@orthoxerox The proposal supports type classes on generic types. Unless perhaps I don't understand what you mean.

orthoxerox commented 7 years ago

@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.

gafter commented 7 years ago

@orthoxerox It does not support higher-order generics.

orthoxerox commented 7 years ago

That's what I meant.

louthy commented 7 years ago

@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 Bind the return type is only constrained to be a Monad<MB, B>, when I need it to be constrained to Monad<Option<B>, B>.

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 Option class instance:

    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 F<A> into FA, A and inject the correct constraints. Then maybe the (apparently impossible) job of updating the CLR wouldn't be needed?

As @orthoxerox mentioned, the generics story gets pretty awful pretty quickly. Here's a totally generic Join and SelectMany

        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 SelectMany with calls to the generic versions. This is SelectMany for Option:

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 FromSeq for Monad<MA,A> for example)

gafter commented 7 years ago

@louthy Without thinking too deeply about it, I would ask the questions

  1. Are higher-order types related to type classes, or orthogonal (but perhaps complementary) to them?
  2. Do they require CLR support (e.g. for using a higher-order API across assembly boundaries)?
  3. Are they obvious/straightforward to specify and implement? Are the design choices obvious?
  4. Would they "pay for themselves"?
gulshan commented 7 years ago

This may lead to a totally new (and separate?) standard library, with this as the base.

MattWindsor91 commented 7 years ago

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.

orthoxerox commented 7 years ago

@gafter

Are higher-order types related to type classes, or orthogonal (but perhaps complementary) to them?

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.

Do they require CLR support (e.g. for using a higher-order API across assembly boundaries)?

Yes. Unless there's some clever trick, but then they won't be CLS compliant.

Are they obvious/straightforward to specify and implement? Are the design choices obvious?

Not that straightforward. The design choices are more or less clear.

Would they "pay for themselves"?

As much as any other type classes would.

louthy commented 7 years ago

@gafter

@orthoxerox has concisely covered the points, so I'll try not to repeat too much.

Do they require CLR support (e.g. for using a higher-order API across assembly boundaries)?

I think this was always the understanding. Obviously the 'hack' that I've done of injecting the inner and outer type (i.e. MOption<Option<A>, A>>) into the higher-order type's argument list is something that doesn't require CLR support. But types like Functor<FA, FB, A, B> have no constraints that enforce the F part of FA and FB to be the same. So this is possible:

    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 Functor<F<A>> into Functor<FA, A> and enforce the outer type F to be the same (for the return type of Map), then good things would happen. That is the single biggest issue I've found with all of the 'type classes'. Also the type inference story should significantly improve by default (well, obviously none of this is free, but currently having to specify Option<A> and A is redundant).

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?

Are they obvious/straightforward to specify and implement? Are the design choices obvious?

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.

Would they "pay for themselves"?

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.

gafter commented 7 years ago

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.

louthy commented 7 years ago

@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.

gafter commented 7 years ago

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.

Thaina commented 7 years ago

@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;
}
gafter commented 7 years ago

@Thaina You can read it for yourself; I believe the answer is "no". What translation strategy would you recommend for that?

Thaina commented 7 years ago

@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 would not mix higher-kinded types into this issue

I still don't understand what higher-kinded types means too

orthoxerox commented 7 years ago

@Thaina the answer is no, it doesn't. Open a separate issue if you want anonymous concepts.

Thaina commented 7 years ago

@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

MattWindsor91 commented 7 years ago

(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.

Thaina commented 7 years ago

@MattWindsor91 I was really misunderstanding thanks for your clarification

orthoxerox commented 7 years ago

HKT emulation via associated types is included in the new prototype: https://github.com/MattWindsor91/roslyn/blob/2017-stable/concepts/docs/writeup.docx

MattWindsor91 commented 7 years ago

@orthoxerox Thanks for the mention! @crusso and I still actively working on the prototype until the 27th.

Some things that would normally use HKTs can indeed be mocked up using associated types. (See the various implementations of LINQ methods in /concepts/code/TinyLinq/TinyLinq.Core 😀). I’ve found a few issues with this approach in the prototype:

1) This encoding balloons to a lot of type parameters (both normal and associated). In the prototype, associated types are just normal type parameters syntactically, which means there’s a huge cascade of them all the way down from the concepts where they’re actually fixed through any derived instances. See SelectMany’s instances for the nightmare scenario. Some clever syntax might make this less horrendous though...! 2) More an implementation issue, but encoding HKTs using ATs makes inference very tricky to do right. Again, SelectMany over enumerables, as encoded in the prototype, is a good (horrible) example: we need to do a round of concept inference to get the AT corresponding to the input collection’s element type, then output inference to get the type of the collection coming out of the collection selector function, then more concept inference to get that collection’s element, then output to get the result type, and all of this has to happen in the context of working out an instance for the SelectMany concept.

I apologise for the horrors within the prototype implementation of that back-and-forth-switching-with-backtracking inference scheme 😰. There are still a few consistency issues I haven’t worked out.

NinoFloris commented 7 years ago

@MattWindsor91 How'd it go? Very curious in the progress of it all :)

ig-sinicyn commented 7 years ago

@MattWindsor91

  1. Well, may be I'm suggesting a stupid idea or something that ends to be technically impossible. All in all, what if concepts will be passed using single generic parameter, something alike

    void Process<T, implicit TConcept>(T state) where TConcept: struct, LogConcept, MathConcept
    {
    Log(Increment(state));
    }

    ?

This should solve your issue with explosion of concepts in generic args.

Undercover Concept1 and Concept 2 are interfaces with default method implementations, TConcept is emitted by compiler and looks like

struct c<>_bla-bla-bla: IMathConceptImpl<int>, ILogConceptImpl<string> {}
// no methods here, taken from default implementations in interfaces

(yes, there is a set of such dummy structs for each assembly. Like it was done for anonymous types).

There is no penalty for calls or boxing as JIT is smart enough to emit effective assembly instructions for constrained. interface calls over TStructs. Random googled example.

Of course there is need to be a syntax for concept impl resolution on caller-side and TConcept method resolution on end-side. Both are a just a syntax issues and I do not think it has to be a major problem.

  1. It looks like concepts will poison long chains of generic method calls. As far as I can see there's no way to call a concept method from normal generic method, ala

    void SomeCode<T>(T state) => Process(state);

    To enable such scenarios there should be a support for dynamic concepts

    void SomeCode<T>(T state) => dynamic Process(state);

    or may be some specialized api|syntax that will allow to improve performance for such calls via emitting and caching call wrappers.

lambdageek commented 6 years ago

@MattWindsor91, @gafter Have you given any thought to instance coherence / overlapping instances?

One of the advantages of (at least Haskell 98/Haskell 2010) typeclasses as opposed to the various typeclass-like implicit dictionary mechanisms in other languages is that the design encourages coherence: whichever path through discovers an instance is guaranteed to be as good as any other (e.g. (Eq a => Ord a) and (Ord a => Ord [a]) gives a path to Ord [a], but so does (Eq a => Eq [a]) and (Eq [a] => Ord [a]) and it does not matter which one the compiler chooses.

For those following along: Edward Kmett's Typeclasses vs The World talk outlines from a practitioner's perspective why coherent typeclasses are more usable than implicit dictionaries.

sighoya commented 6 years ago

@MattWindsor91 I have the feeling that you do not introduce typeclasses directly into C# rather indirectly via "modules" (modular type classes), such that concepts are signatures and c# structs/instances implementing concepts are modules. Advantages are that modules (though we need support for functors) are more powerfull than the typeclass system in Haskell and solve additionally some problems that Haskell have with type classes (global uniqueness problem) Disadvantages are that they harder to understand and the syntax is more verbose than for normal typeclasses.

What was your intention for? Elimination of the global uniques problem in Haskell?

andre-ss6 commented 6 years ago

Sorry for the ignorance, but can someone clarify this for me:

1) How is this related to #164? Does this proposal make it possible to implement Mads' extension proposal on top of it? I mean, in relation to #164, is this just a different way to implement type classes into C# or is this another thing entirely? As I understood (and I don't feel I've really understood this), this issue, in relation to Mad's, just proposes implementing type classes in a more optimized manner. But, in the end, mad's proposal could easily be built on top of this. Is this understanding right?

Edit: There's actually a side-by-side comparison of Mad's proposed code to concept-c# code at Concept-C# repo

2) Will following this route [of implementing type classes via compiler tricks] make implementing HKP with CLR support in an ideal future easier, harder or indifferent? I mean, in a future where you decide to fully support HKP, will this proposal be looked upon as a necessary building block or as a "bad design decision that seemed good at the time"? Personally, I'd rather have this and HKP with CLR support in 5 years as opposed to having this only, but now.

Again, sorry for any lack of knowledge. It is already quite difficult to follow along all the FP math, but it is way harder when you throw compiler, runtime, spec/syntax magic into the mix.

gafter commented 6 years ago
  1. This is completely orthogonal to higher-kinded-types. I don't think having it would make it either easier or harder.
orthoxerox commented 6 years ago

@gafter Are associated types a part of this proposal? If they are in, they might make switching to HKTs later harder.

gafter commented 6 years ago

@orthoxerox Associted types are supported in the latest prototype. It isn't clear to me whether that is intended to be bundled. Can you please unpack the assertion that they would make HKTs later harder?

orthoxerox commented 6 years ago

@gafter If I am not mistaken, everything associated types do HKTs do as well, but they do much more (e.g., you can't encode SFunctor<F<T>> using associated types).

If something like SEnumerable<TCollection, [AssociatedType]TElement> is added to the standard library, future versions of C# will have to support both this shape and a higher-kinded SEnumerable<TCollection<TElement>>. I am not talking about these specific shapes, of course, but about the inference and lowering and codegen (since HKTs will require a new CLR version) mechanisms behind them. I am afraid that associated types will then become another __arglist or anonymous delegates, deprecated in practice but still complicating further compiler work.

VisualMelon commented 6 years ago

I keep trying (and failing) to work this out: what is "Structural Generic Constraint" in reference to?

As far as I can tell (and I hope I'm not wrong), there is nothing structural about the proposal (i.e. shapes are strictly nominal types), so I'm at a loss as to what a "Structural Generic Constraint" is. Does it have something to do with structs? Is there a sensible reference for this term that I just can't find?

sighoya commented 6 years ago

@VisualMelon

I think it relates a bit to my question

Does it have something to do with structs?

Well, I think yes.

Afaict,... the way which Haskell's type classes are handled is not the same for C# concepts. C# concepts implementation relates much more to OCaml Modules

The difference is that C# Concepts or Interfaces are treated as normal struct signatures and each instance of it is a struct which inherits from the struct signature (which is in fact different to OCaml's Modules) Now you would say that the struct "instance" implements the struct signature "interface" and does not inherit from it, but under the skin interface implementation is like inheritance in which the interface is a pure virtual abstract class where inheritance in most standard OOP languages is nominal + structural subtyping.

Beside that type classes in Haskell are pure magic, they should be predicates or relations. There are also alternative interpretations, but that goes out of scope here.

In the end, implementing types of a typeclass have some isa "like" relationship in Haskell whereas a subtyping relationship in C#.

gafter commented 6 years ago

I keep trying (and failing) to work this out: what is "Structural Generic Constraint" in reference to?

The phrase Structural Generic Constraint means that we want the ability to express generic constraints that describe the structure, or shape of the type argument. That includes things like whether the type argument contains particular constructors, static methods, operators, conversions, etc. The word structural here is used in its sense as an opposite to nominal, which existing constraints are. Existing constraints are nominal in the sense that they name types that the type argument must derive from. Structural constraints describe features that the type argument must possess, and if you want to use a type that does not "natively" possess that feature you can provide the "glue code" that provides an implementation for that feature when instantiating the generic.

VisualMelon commented 6 years ago

@gafter do correct me if I'm wrong, but there is no structural typing going on: you still need an explicit (nominal by type name) implementation of the shape... no? For some T to pass as having some shape S, something needs to reference T and S (by type name) and provide any necessary member/operator implementations, so it's still nominal because we only allow access to nominally declared types (at least by my definition, but perhaps my definition is wrong) even if the declaration isn't in the same place as the declaration of T.

(Terminology aside, is my understanding not completely-off-the-mark?)

HaloFour commented 6 years ago

@VisualMelon

No, the purpose of this proposal is that the type doesn't actually have to implement/inherit from the shape, it just has to support all of the members required of that shape. The compiler would then emit a struct which actually implements the shape interface which provides the glue between nominal typing, which the CLR expects, and the instance in question.

VisualMelon commented 6 years ago

screams

Now that I've calmed down a little... Thanks for clarifying.

sighoya commented 6 years ago

Existing constraints are nominal in the sense that they name types that the type argument must derive from.

For some T to pass as having some shape S, something needs to reference T and S (by type name) and provide any necessary member/operator implementations, so it's still nominal because we only allow access to nominally declared types (at least by my definition, but perhaps my definition is wrong) even if the declaration isn't in the same place as the declaration of T.

No, the purpose of this proposal is that the type doesn't actually have to implement/inherit from the shape, it just has to support all of the members required of that shape.

Afaics, both things are structural and nominal. The point is shapes/concepts allow for posthoc/decoupled implementation/inheritance of a shape/concept where this is abused in standard class inheritance and interface implementation.

@gafter Interesting, so "structural" relates to compositional properties.

CyrusNajmabadi commented 6 years ago

From the runtime/implementation perspective, you would still be using nominal types to accomplish this task. After all, the runtime will insist that things like interface constraints are actually satisfied by the types being passed in.

However, much of this will be an implementation detail. From teh languages perspective there is no nominal types.

VisualMelon commented 6 years ago

Looking at the proposal again, I still can't find anything to suggest that it does any structural typing: can someone point me to some evidence of this? Unless the "glue" Gafter was referring to is injected by the compiler and not the programmer (i.e. is an implementation detail), I still don't see anything necessarily structural in that description. Perhaps my understanding of the term is too narrow.

P.S. I'm not concerned about the implementation (unless it is the explanation for why the proposal is aliased "Structural Generic Constraints")

HaloFour commented 6 years ago

@VisualMelon

Unless the "glue" Gafter was referring to is injected by the compiler and not the programmer (i.e. is an implementation detail), I still don't see anything necessarily structural in that description.

That's exactly what will happen. The compiler will generate a struct which will implement the interface that represents the concept.

CyrusNajmabadi commented 6 years ago

Looking at the proposal again, I still can't find anything to suggest that it does any structural typing

The shape is itself structural. For example:

 concept Eq<A>
  {
    bool Equal(A a, A b);
  }

Or:

public shape SGroup<T>
{
    static T operator +(T t1, T t2);
    static T Zero { get; }
}

All this is saying is: as long as you can provide those members somehow (extension, directly on yourself, whatever), you can be passed to code that works with Eq's or SGroup's

There is no need to actually 'implements' anything, and the compiler will allow any type to work here as long as it demonstrates (implicitly or explicitly) how it abides by these shapes.

This is very different from the runtime, and how it does nominal types. If you say you take an IGroup or an IEq, your actual instance must actually implement that interface a-priori.

No such rule applies for shapes/concepts/type-classes (whatever want to call them).

Now, from an implementation perspective there will be actual nominal typing going on. Just with types that you never see or interact with in any way. For example, the shapes proposal (https://github.com/dotnet/csharplang/issues/164), uses nominal typing under the covers to synthesize both an interface (for use in constraints you never talk about), and a struct htat implements that interface (for the code to actually go in).

These types are actually necessary to abide by the CLR's type system. But as far as C# is concern there is nothing nominal. You don't inherit or implement things, and you can have vastly different and unrelated instances be valid args to where you accept a shape/concept/etc. Your type could be a class, struct, interface, delgate, whatever. It could implement whatever interfaces it wanted (including none). it can sit anywhere in any inheritance hierachy.

CyrusNajmabadi commented 6 years ago

Unless the "glue" Gafter was referring to is injected by the compiler and not the programmer (i.e. is an implementation detail), I still don't see anything necessarily structural in that description.

yes, that's precisely what the proposals suggest should happen. All the parts about this implementation are simply glue so that hte language can efficiently take thse concepts (no pun intended) and tie them onto how the runtime actually works.

The language then exposes what appears to be a structural type system rather than a nominal one. i.e. you satisfy a shape if you can show how you satisfy its shape, rather than satisfying a type by implementing/subclassing it nominally.

This also has the virtue of decoupling that satisfying from the declaration point of the type. For example, @HaloFour could create a type Foo, and i could use that type in my own code as something that sastifies the Bar shape/concept even if Halo knew nothing about it. That could happen implicitly if Foo just naturally followed the shape of the Bar constraint. Or it could happen through 'witnessing/extension/some name' where i demonstrate to the system how Foo satisfies the structural shape.

VisualMelon commented 6 years ago

It seems my fears have been realised (I'm well aware of the benefits of type-classes, and 'benefits' of structural typing): the text "implicit instance construction with explicit fallback" appears in summary table at the end of the proposal, which rather seals the deal :(

(I'm well aware of how all this stuff could/would work, I've just been hoping that everyone is confused, and that the proposal doesn't suggest structural typing at all, but it seems that it does)

CyrusNajmabadi commented 6 years ago

It seems my fears have been realised (I'm well aware of the benefits of type-classes, and 'benefits' of structural typing): the text "implicit instance construction with explicit fallback" appears in summary table at the end of the proposal, which rather seals the deal :(

What is your fear of that? It would be helpful if you could explain that. Thanks!

VisualMelon commented 6 years ago

As you put it, "That could happen implicitly if Foo just naturally followed the shape of the Bar constraint": this is not a notion I find appealing. As a rule, I do not like structural typing (at least not for named types), and I really do not like the fact that this proposal could just as well have been purely nominal (as part of an essentially nominal language), but is not.

HaloFour commented 6 years ago

@VisualMelon

We already have nominal constraints, and you're free to continue to use them. Much of the point of these proposals is to allow the use of generics in those situations where you can't use a nominal constraint either because you can't control the type which you wish to use as a generic type argument or because you wish to use features that nominal constraints cannot enforce due to their limitations on being based on interfaces, such as requiring static members, constructors, operators, etc.

Why is the concept of structural typing such an issue? It's still statically resolved at compile-time and completely type-safe. The only real difference is that the target type doesn't have to do anything special to conform.

Pzixel commented 6 years ago

@HaloFour the only problem I see here is that some type may suddenly implement some concept without really wanting it as well as you can't get list of all shapes that object currently can be resolved to. E.g. this is what Go community thinks.