dotnet / csharplang

The official repo for the design of the C# programming language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

gafter opened this issue · comments

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 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:

  • I can't put a this in front of MA self. If I do, then every type gains a SelectMany and Join method, even if they're not monadic.
  • The type-inference story is obviously non-existent, and LINQ cannot work this out

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):

  • Significantly improved generic-parameter type-inference story. i.e. not providing arguments when not needed would be a good start.
  • Constraints that link return types to generic arguments

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)

@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"?

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.

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

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

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

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

@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

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

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

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

@MattWindsor91

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.

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.

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

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

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

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

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

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

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

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

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?

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

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.

@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?)

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

screams

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

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.

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.

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")

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

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

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.

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)

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!

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.

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

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

@HaloFour None of that wouldn't be the case if this was purely nominal. It would still provide static constraints (on static members and operators etc.), and it would still allow ad-hoc implementation of these static interfaces for existing types (which are what can't cleanly be done with interfaces already)

I dislike structural typing because:

  • it allows you to make mistakes (you can use a type incorrectly because it 'just happens' to be the right shape, or you can get implementation wrong because it clashes with existing members or those from another type)
  • it means that concrete types do not express their intent explicitly, which makes code harder to reason about
  • conceptually it devalues the name of the type, instead presuming that the signatures of the members define the type, not the name, which also makes the code harder to reason about
  • (in this instance at least) you can't opt-out of implementing a type, which depletes the space of useable member signatures (though actually, the moment you don't have to opt-in a similar concern presents itself regarding unknown/new types)

I kind of don't care about the "you can make mistakes" bit; it's just a conceptual nightmare which (like duck-typing) elevates members to a position of power, and devalues named types (which are the valuable unit for modelling and reasoning about)

Sounds like one of those philosophical disagreements for which there isn't a resolution. In the case of concepts the "type" that describes the shape isn't supposed to be important. It's a container that exists only because that's how the signatures can be described to the CLR. The shape is the sum of the member signatures.

Notably this proposal doesn't change the behavior of interfaces. They're still strictly nominal. Concepts/shapes would be defined with different syntax. So unlike Go you have a choice. It's also quite possible that tooling could be improved to help find "implementers" of the shape.

As for the complaint that a type could "accidentally" implement the concept just by having the same shape, yes, that's the point, and it's not considered to be a problem.

Interfaces are not as powerful as shapes, and people will use shapes. Without a nominal counterpart to this proposal, the choice is not "nominal" vs "structural", it is "exclusively virtual nominal generic constraints" vs "all manner of structural generic constraints" vs ugly code. Furthermore, other people will use and expose them (which is true for many proposals), so there is no choice (especially so here because you can't opt-out of implementing them unintentionally).

In the case of concepts the "type" that describes the shape isn't supposed to be important

A notion that I would appose on grounds I have already expressed.

Whether people care about these concerns is subjective (personally I think structural typing is pointless and unhelpful), and indeed not resolvable (at least some people at Google disagreed with me); however, I do get the feeling that few people are aware of the implications (and not just because it took me over 6months to get around to asking whether I was missing something and finally plant a -1 on this; I'm easily confused), so thanks for asking, else I wouldn't have bothered to write up my complaints.

@VisualMelon I do not understand how your types will be violated by being matched against shapes. First, implicit conformance to shapes is not a given at all, and second, even if C# will auto-derive a witness for your class, the person using your type will still have to look at the signature of the shape-accepting method, look at the methods implemented by your type and make an explicit choice of passing an instance of your type to the method.

@VisualMelon It is not yet clear if this final version of this proposal will require an "instance" declaration to explicitly declare that a given type satisfies a given type class (shape), or it such a thing will always be inferred. One reason to think that an explicit declaration may be required is that, to fit with the underlying nominal type system of the CLR, a specific named type must exist as the glue between the shape and the type that satisfies the shape. Having the user provide that is one way to ensure that such a named thing exists. I personally feel that's a good idea from a language design standpoint. Even if it is required, however, we may permit the compiler to "fill in" the parts that are obvious satisfying parts from the type, and we may permit the compiler to automatically (implicitly) find the glue instance without it being mentioned everywhere it is used.

@gafter thanks for the info, it's most welcome. Indeed, automatically 'filling in' the bits that are already there would be consistent with implicit member implementation for interfaces, and I can get behind that from a 'I don't want to write 100 redirections' point of view.

Some remarks:

Which method will be invoked here:

1.)

void method<A,implicit EqA>(A a, A b);
void method<A, implicit OrdA>(A a, A b); 

2.) And here:

void method<A,implicit EqA>(A a, A b);
void method<A>(A a, A b);

3.) Does this proposal include the option to allow for multiple instances of the same type:

void first<A,implicit EqA1>(A a, A b);
void second<A,implicit EqA2>(A a, A b);

@orthoxerox sorry, I wrote a long reply, but there was nothing much new in it, so I don't think I'll actually post it. Basically, unintentional implementation isn't my main concern; however, it is a horrifying prospect for me because there is no sensible way to mitigate it (you'd have to rely on good documentation and diligence), and it makes a mockery of the notion of expression of intent (of which I'm a big fan).

@VisualMelon

it makes a mockery of the notion of expression of intent

I disagree. The declaration of intent is the attempt to call the method that consumes the shape. This doesn't affect the identity of the type(s) that you're passing at all and thus that type doesn't have to (and shouldn't have to) make any declarations.

Can structural types require members that are of a type internal to the assembly?

Well, unintentional implementation seems to be more or less solved in shape prosposal, where you use extension everything feature to conform to shape after-the-fact but still explicitly. Because of this you cannot make unintentional mistake unless you dont fully understand what shape or extended instance is supposed to do in which case its your own fault. The only problem is that it will be hard to discover outside of IDE.

I know its not your biggest concern but i wanted to point out that in case of shape+extension everything combo, this problem is minimised and will exist mostly outside of IDE and even then, its not that bad, just look for extensions that extend said instance

@BreyerW if there is 10 matching shapes and every shape provide AsEnumerable method how you get which one is actually executing (without asking IDE)?

I know its not your biggest concern but i wanted to point out that in case of shape+extension everything combo, this problem is minimised and will exist mostly outside of IDE.

Au contrere, with extension everything you may shadow unintentionally something you were expecting to call.

Matching is the keyword, in this case code will likely not compile because of ambiguity and the situation is same as in not sufficiently overloaded methods. But lets say we have such bizzare example. In this case i think you should think about consolidating shapes. So many MATCHING shapes for ONE instance will likely point to potential overlaps.

@HaloFour That is one expression of intent (not a public one), but if the type hasn't expressed its intent then how am I suppose to know how to use it? A type's external API is orders of magnitude more important than the statements inside a method.

@BreyerW (regarding you previous comment) indeed; but as soon as you make it explicit you can do away with the (redundant) structural typing. Relying on the structural typing just means you risk making the same mistake every time you try to use a type/shape combo, instead of going to effort of making it once when you write an explicit (nonsense) mapping (which is nice and easy to resolve when you realise).

it means that concrete types do not express their intent explicitly,

There is nothing about structural types that mean you can't express your intent explicitly. I use typescript a ton and it is structural. However, i express my intent there all the time. I can choose not to in some cases, but i often do when i think it's valuable to do so.

conceptually it devalues the name of the type, instead presuming that the signatures of the members define the type, not the name, which also makes the code harder to reason about

Note that the is already the case in many APIs. The name doesn't really matter, and is even a hindrance. That's why, for example, there are such vague names like Func and Action used extensively for delegates. Because what is actually important is the flow of data in and out. Structural types are just a way of composing those to higher level kinds.

it's just a conceptual nightmare which (like duck-typing) elevates members to a position of power,

I use two structural langauges already on a day to day basis. At no point has it been a conceptual nightmare. it's just another tool in the toolbox. My team uses them effectively and has no trouble reasoning about things.

and devalues named types (which are the valuable unit for modelling and reasoning about)

So what? I don't care if named types are devalued by virtue of having more options. It's like telling me 'classes are devalued because now i can pick a struct'. Or interfaces are devalued because i could use a delegate in place of some subset of them. So what? If nominal types make sense for the domain i'm in, i'll use nominal types. If shapes make sense, i'll use shapes.

E.g. this is what Go community thinks.

This is what a single member of the community thinks :) (though i'm sure there are many more who agree). Without question you can find community members for every language that will take issue with every part of your language. That's just how it works :)

As a whole though the go community has remained remarkably accepting of these decisions without a large sentiment i can see through any go channels (har har) that this needs to change.

Like any language choice, there are pros/cons, but overall the community seems to have no problem wit hthis side of go in balance.

(at least some people at Google disagreed with me); however, I do get the feeling that few people are aware of the implications (and not just because it took me over 6months to get around to asking whether I was missing something and finally plant a -1 on this; I'm easily confused), so thanks for asking, else I wouldn't have bothered to write up my complaints.

If it helps, i was one of the people that created the structural type system for TypeScript. A particularly challenging project given that you are not defining the structure that you do want, but rather trying to find ways for users to express the structure that is there.

I also use go as one of my primary languages on a day to day basis. I think i'm pretty well in touch with all three ecosystems and communities and i'm pretty well aware of the implications. I just don't see any of this as a net negative. This is merely how any language feature works, where there usually are lots of pros, along with possibly some cons to things.

@VisualMelon
Hmm im not sure if you realise but type classes and shapes are compromise solution to bring more powerful constraints. Theoretically, everything could be achieved by extending family of available purely nominal constraints in CLR but changing CLR is such a pain in the ass that team will avoid it at all cost even accepting inferior solutions as long as they dont require CLR changes.

In lieu of this shapes+extension everything is as explicit and nominal as we can get. Personally if i would have to choose no feature (especially this powerfull) or feature slighty flawed (and only if you are reeeaaally bad programmer in which case there is hundreds more traps already existing and awaiting these fools) i wouldnt think long.

BTW shapesare actually very explicit. You can use and treat them like any nominal constraint as long as you have absolute control over code base since you can ( or rather pretty much HAVE TO) extend any instance with shape exactly like interface. Extension everything will be used mostly for 3rd party classes/structs. See examples at the top post in #164

Relying on the structural typing just means you risk making the same mistake every time you try to use a type/shape combo,

Can you explain this bi. What "same mistake" are you referring to. I've used structural typing in typescript and go extensively for the past 5+ years, and i can't think of a time when a mistake has happened here. I'm trying to even grasp how likely it would be that you coudl make a mistake. That you had a shape that was pretty much identical to the form of your domain object. And yet... you would not feel that your domain object matched that shape.

@CyrusNajmabadi I'd be up all night trying to reply to all that (I write very slowly, and I'd probably just end up saying the same things again), but I'll try to respond to some of it...

@BreyerW this proposal could be perfectly nominal; it would be easier to implement because the compiler wouldn't have to interrogate types when trying to pass them implicitly as a shape. Nothing about shape + extension everything has to be structural.

@VisualMelon

Hm if you are so sure then care to make alternative proposal and link it here and in #164 ? Im certain team will be delighted to see proposal that might work and will be easier to implement. Remember CLR changes are pretty much a no-go.

@CyrusNajmabadi I'd be up all night trying to reply to that (and I'd probably just end up saying the same things again)

The important bits for me are explanations as to the extent to how bad things are. For example, i certainly can't disagree that one could accidentally conform to a shape. What i'm trying to figure out is how that can be felt to be likely enough to warrant concerns. Again, this going against many years of experience using structural languages where i can't recall this happening once.

Similary, i would like understanding behind views of 'devaluing'. I do not see additional available tools as devaluing anything. Indeed, my view on C# is that it exists precisely by taking the starting point of Java and thinking deep and hard about many sorts of additions that provided value, despite arguably devaluing the 'core' java basis it carries its lineage from.

One of my favorite things about C# is that it is an unabashedly 'jack of all trades' language. Want OO constructs? You've got them. Want functional? Go for it. Want dynamic, there as well. Like pattern matching? There; with more coming. This fits into that world for me. There isn't one true language for me. There are just tons of language features that be combined in interesting ways to make life better for authors and consumers alike. This feels like a wonderful addition to that set of capabilities that i think will be very beneficial to the entire ecosystem.

Nothing about shape + extension everything has to be structural.

Technically, nothing about shapes/extensions/concepts/etc. even needs C# language support. You can already write it all yourself today with teh constructs the runtime supports. Heck, the shapes/extensions proposal shows precisely how it would 'lower' into the constructs the runtime already has.

That argument isn't interesting to me simply because that is well understood and precisely the point o of the language proposal. The way to do thsi today with nominal is just unpleasant. Sure, it can be done, but people rarely do it because it's just not a great experience. On the other hand, shapes/extensions/concepts attempt to reformulate the pattern in a way that feels more natural and easier to pull off and understand for an existing C# developer. The entire purpose of hte feature is to repackage what you can already do, but in a way that is actually palatable and can ideally be successful.

@BreyerW honestly, I'm now not sure again whether this proposal is structural. I have a really hard time reading the documents, and there have been some contradictory comments made. The implementation would be exactly the same, you just don't implicitly write instances when you can match the members of the concrete type to the structure of the concept/shape/thing (you error instead)

@Pzixel

Sorry didnt saw your comment to quote. I dont think it will be that big problem because extension everything force you to explicitly state what is extended and optionally state which interfaces and shapes you will abide. If you dont, instance will not abide any new interface and/or shape.

Outside of ide it might be problematic but all you will have to do is use search function and check if there is extension of MyClassIWantToCheck or whatever syntax will be

@CyrusNajmabadi sorry, this is quite long. I tried to make it shorter but it just gained more paragraphs so I'll stop trying now.

The important bits for me are explanations as to the extent to how bad things are

I can't give a good answer to this, because I have essentially zero experience (except with auto currying...), because I stay away from non-nominal languages as best I can. I shouldn't wish to tell anyone whether these issues are likely or not, only that it is definitely possible, that I don't see a good way to deal with it if it should occur. I do think they will be more of an issue in an already mature nominal language than they would be in a structural-from-the-outset initiative like Go.

Regarding expression, sure you can express your intent in a structural language, but nothing says "I implement interface IFace" better than saying "I implement interface IFace", and I think this is a really useful thing to do (that is, to unambiguously express (to the compiler and programmer) that you are meant to implement IFace).

conceptually it devalues the name of the type, instead presuming that the signatures of the members define the type, not the name, which also makes the code harder to reason about

Similarly, I would like understanding behind views of 'devaluing'

My point here is that to implement the type you no longer need to reference it by name, and (as someone else has said) it becomes a carrier bag for a set of methods. Conceptually I'm sure we can all agree that an interface should provide a meaningful and consistent interface, which comprises a set of well named members. The interface is not defined by the members, it is defined by the intent which is represented by its name, which is served by the members. Lists don't have an Add method, they have a List.Add method. Structurally typed interfaces/traits/whatever 'devalue' the the List. bit, because the implementer doesn't implement List.Add, it just implements Add (from the perspective of the structural typing engine).

I'm probably failing to make the context of my commentary clear, but the point is that this is a new powerful feature, but it is structural: this means that that the name of a shape/concept/whatever (structural) has less meaning (it is 'devalued') than the name of a C# interface (strictly nominal), and that the members which a type exposes start to define how it can used, rather than just being an incidental public API attached to the concrete type.

Structural types impart meaning onto member names and signatures in other types, meaningful or otherwise.

Somehow I don't think that will help clear up my position... but I tried.

Or interfaces are devalued because I could use a delegate in place of some subset of them.

No. Again, I've clearly not made my point well, but one of those few things in software that makes me smile is the knowledge that delegates are nominal: they have a name, and that name expresses intent, even if we have all agreed the Func and Action are meaningless. No matter: Func<int, int> has no bearing on my delegate int Cycle(int).

One of my favourite things about C# is that it is an unabashedly 'jack of all trades' language

One of my favourite things about C# is that it's nominally typed through and through. The 'functional' stuff is all implemented nominally (though as far as I am concerned, there is practically nothing functional about C#). Dynamic - which is explicit dynamic, not incidentally not nominal - is completely opt-in (and thankfully I never encounter it). The pattern matching is nominal. Interfaces are nominal. I am never in any doubt whether the code I write is meant to work or not (as opposed to will run or not); everything is completely explicit, and the compiler helps me every step of the way. Edit: Tuples are essentially/actually structural, and I don't like them much either, but they are opt-in and they can't influence other types (I'd still object to anyone using them as part of a public API)

My complaint here is that this feature will be used, and that it will produce public structurally typed APIs. It's another tool in the box, but for some reason it departs fundementally from the other tools in the box, and I don't see any benefit to doing so. It allows me to export a shape/concept/whatever, and a concrete class, and give you no explicit indication that one implements the other. No longer can the compiler help me, because I havn't told it what I want: I'm expecting it to work it out for me.

If this proposal were nominal (which as a point of information, I'm not sure it isn't at the moment), then I would be inclined to agree it would be a wonderful addition (though I'd still rather just have static interfaces (i.e. no implicitly selected instances, non-inheritable, etc.), but that's a different issue).


Since I've been attacking structural typing and hailing nominal typing for while, would someone like to make the case that there is actually some benefit to having shapes structurally typed?

I might need to take a break from all this, I'm running out of hair to pull from my head, and I don't want another repeat of in (honestly, I wasn't looking for a discussion on this, I was just trying to work out if it was structural or not so I could give it a -1 if appropriate).

@BreyerW

Outside of ide it might be problematic but all you will have to do is use search function and check if there is extension of MyClassIWantToCheck or whatever syntax will be

It won't really help you. Consider you have interfaces IA, IB, IC, where IC : IB, IB : IA. You have an extension on IA for method Foo(A, B), extension on IB for method Foo(A, B, C = null) and extension on IC for method Foo(A, B, params[] object foos). And then you call myIC.Foo(a,b). In this case it would be very fragile and it's not easy to determine which function is the best match. I can't say it right now without going to MSDN and rereading method resolution rules. Can you?

@CyrusNajmabadi

This is what a single member of the community thinks :) (though i'm sure there are many more who agree). Without question you can find community members for every language that will take issue with every part of your language. That's just how it works :)

I'm not saying it's a blocker and it's a no-no feature. Just like you proposed add yet another tool in a toolbox, I provide additional point of view in your point of view bag to make decision more deliberated 😄

@VisualMelon

Afaict, it is still explicit but not at the global level more on a local level, i.e. on the method definition side. So you can't call shape methods on a instance of a type as long as it is not specified in the method signature.
This make things much more flexible and circumvent the global uniqueness problem of Haskell. So theoretically you should be a able to select different implementations for the same type by different method signatures, at least in my mind.

You have to imagine that the "implicit" keyword conduct the implementation for you with some already implemented struct which can be shared for more than one type providing you, theoretically, a m:n relationship between instance selection and instance selected by.

In the end, you didn't inherit the mess from C++.

Please correct me, if I'am wrong.

@VisualMelon

Since I've been attacking structural typing and hailing nominal typing for while, would someone like to make the case that there is actually some benefit to having shapes structurally typed?

I can imagine it's extemly helpful when dealing with bad codebases (which are 99% of overall quantity). For example, let's take BCL itself. We had no IReadOnlyCollection for an ethernity. Finally it's here. But there still billions of libraries that just inherit IEnumerable and provide Count property, without implementing anything else.

When I was studying in university and I was writing my own collections I was never implementing ICollection because it was to restrictive - look at all these CopyTo, GetElementAt, and so on. So i was just exposing Count property and worked via LINQ successfully. But if I wanted to work with it via interface I had no other interface except IEnumerable. If you have R#, you are probably familiar with warning "possible multiple enumeration". So you have two options:

  1. Take IEnumerable and pray that it doesn't query data twice, trice or more.
  2. Take IEnumerable, call ToArray and make extra allocation. You are also lying to your clients that you take IEnumerable because you are actually work with array
  3. Take IReadOnlyCollection as argument, in this case you are not lying to your clients, but you limit them and now they have again to perform extra allications or just not use your method
  4. Take anything that has GetEnumerator and Count method and have best of two worlds: zero allications and suitable interface.

@Pzixel

Yeah with deep hierarchies it will be a mess but deep hierarchies are already a mess in itself due to complexity and everyone lives despite this problem so im not overly concerned. And there are still IDEs its not like we dont have great tooling for free nowadays.

@topic

I think it might be worth treating shapes and concepts separately. They achieve similar things as far as i understand but offer different approach: former is extremely explicit thus feel more nominal while the latter is more implicit in nature (hell, they even propose to add new keyword implicit to strengthen this nature further)

@sighoya sorry, I'm not sure I follow (probably my fault, I'm easily confused). Which mess are we not inheriting from C++, and is the structural typing necessary to avoid it?

I'm not sure that the idea is to allow selection of a specific implementation by method signature, but rather by (optional) generic constraint (though I'm not sure this is a given).

My understanding of the proposal is (as of about 2 days ago) in a continuously but slowly shifting state, so there is a good chance I've got some of the details off or am reading things that aren't there.


@Pzixel if I'm understanding your concerns, solving them doesn't require structural typing. A nominal version would still allow you to define your own interface and provide an implementation for existing types (e.g. in a different assembly). The effect of structural typing here (as I understand) would be if (e.g. in 3) you provide a custom shape, then people can 'implement' it without having to write (once) a dedicated mapping.

The benefit of structural typing would then be less typing and reduced friction when using your custom APIs with a third-party/custom type (you can provide a generic instance to cover 'the usual suspects', for example from IEnumerable<T> which (as I understand) can perform ideally (i.e. the instance can have a static constraint on the interface IEnumerable<T>, though checking back, they don't appear to show this explicitly in the proposal)).

I suppose there is a benefit in terms of unilaterally changing an API without breaking stuff in the "these types don't really implement what they are meant to" example (personally I object to this sort of trickery under any guise). However, if people are lying about implementing stuff, there is a chance they did so implicitly (otherwise the shape wouldn't work), and now you just have lots of types which 'look' like a shape but are not (they would still be lying, just about something else).


Is there a way that one can test Windsor's example implementation without messing with a local install of VS?

@VisualMelon

if I'm understanding your concerns, solving them doesn't require structural typing. A nominal version would still allow you to define your own interface and provide an implementation for existing types (e.g. in a different assembly). The effect of structural typing here (as I understand) would be if (e.g. in 3) you provide a custom shape, then people can 'implement' it without having to write (once) a dedicated mapping.

This is what this proposal does - it writes all required wrappers automatically. I don't like to type what could be otherwise inferred.

As I see it the reason for this proposal in a nutshell is provide autmatic wrappers that you could write yourself, but you don't.

More general, it allows you to create some post-hoc generalization. You always can write some additional wrappers. But in most cases you don't have enough time: this task should be done yesterday and thus you just write ICollection<T> and write some "here be dragons" cautions in comments, because you just don't have time to wrap it correctly. So this "less typing" actually allows you what you theoretically could do yourself, but in practice you never did.

@VisualMelon

Which mess are we not inheriting from C++

C++ massive implicitism is a mess especially in the context of ad hoc polymorphism.

and is the structural typing necessary to avoid it?
No, explicit conversions.

I'm not sure that the idea is to allow selection of a specific implementation by method signature, but rather by (optional) generic constraint (though I'm not sure this is a given).

Well, generic constraints should be a part of the method signature otherwise we don't know if a certain type satisfies the constraints.
For example C++ doesn't allow for generic constraints they are implicitly inferred by the compiler (type deduction) which is a mess, too. But it will hopefully mitigated with new C++ standard.

To comeback to the selection of specific implementations, I could imagine a scenario where we have two (or more) implementations of set orderings:

instance OrdBySubsumption<T> : Ord<Set<T>> {
  public static bool operator <= (Set<T> set1, Set<T> set2) => return subsetEqOf(set1, set2)
...
}
instance OrdByCount<T> : Ord<Set<T>> {
  public static bool operator <= (Set<T> set1, Set<T> set2) => return set1.size() <= set2.size()
...
}

Then you select by method signature (or generic constraints):

Set<Set<T>> sortBySubsumption<T,implicit OrdBySubsumption<T>>(Set<Set<T>> set) where OrdBySubsumption<T>: Ord<Set<T>>;
Set<Set<T>> sortByCount<T,implicit OrdByCount<T>>(Set<Set<T>> set) where OrdByCount<T>: Ord<Set<T>>;
...

@gafter

It is not yet clear if this final version of this proposal will require an "instance" declaration to explicitly declare that a given type satisfies a given type class (shape), or it such a thing will always be inferred.

I'm curious if you have an example as to what this "instance" declaration might look like. I think the structural typing benefits most when the conversion is implicit and to require the consumer to do extra work in order to use the shape, to me, just feels like it would be noise.

@Pzixel and I don't like my computer guessing at what I want; I like knowing that I've told it what I want, so that when I (or someone else) gets it wrong it can tell us back.

I don't understand that bit about ICollection and dragons.

@HaloFour there is an example of an instance declaration attached to Windsor's implementation: https://github.com/MattWindsor91/roslyn - such instances are necessary to allow the 'post-hoc' definition of behaviour (i.e. implementing interface/concept/whatever members that are not already (or not correctly) 'implemented' by a concrete type).

@sighoya I'm still trying to get my head around that (I don't know nearly as much C++ as I would like)... but yes, I think you are right about the overloading (I didn't see what you were getting at the previous time). scratch that, I'm confused again...

@VisualMelon

I see, so the "extension" type (or "instance" type) would more explicitly wire up the concrete type to the members of the shape that can't be automatically. I do see that in the shapes/extensions discussion:

public extension IntGroup of int : SGroup<int>
{
    public static int Zero => 0;
}

My question would be what would the compiler automatically wire up between the concrete type and the shape? Just existing members? General extension methods/members? When is it absolutely necessary to furnish that instance type?

@HaloFour I hadn't thought about extension methods/members, and certainly haven't given them enough thought to make a sensible remark just now, but an extension member isn't really a member at all (there is no IEnumerable<T>.Select, there is only System.Linq.Enumerable.Select).

Including extension members would imply that a different implicit instance would be created if a different set of extension members were in context; however, it would allow another route to implement stuff (as you imply). There would be concerns with overloading (e.g. does the implicit instance prefer an extension member, or a 'real' member?). Explicit wiring would remain necessary if one wanted to disambiguate, and to provide post-hoc static members (can't implement static int Int32.Zero with extension members presently, but I'm assuming there is no reason not to add extension properties).

I can't give a good answer to this, because I have essentially zero experience (except with auto currying...), because I stay away from non-nominal languages as best I can.

This concerns me. You've been adamantly railing against structural typing, even saying things like:

"screams" or
"it is a horrifying prospect for me" or
"it's just a conceptual nightmare" or
"It seems my fears have been realized"

These are very strong statements, and have put you in the position of stating you are diametrically opposed to this, even though you have little experience htere and stay away from these things as much as possible.

Perhaps consider that your perception is not really fair and that if you spent more times with languages like this you might feel differently: http://www.paulgraham.com/avg.html

Structurally typed interfaces/traits/whatever 'devalue' the the List. bit, because the implementer doesn't implement List.Add, it just implements Add (from the perspective of the structural typing engine).

Again, in practice, i have never encountered this. This feels like a very hypothetical concern on your part as opposed to a true issue that happens in practice. I use Go daily. I helped design the TS language. I never felt this devaluing that you ar ementioning, and i've been using or working on nominal languages since 2001.

No. Again, I've clearly not made my point well, but one of those few things in software that makes me smile is the knowledge that delegates are nominal: they have a name, and that name expresses intent, even if we have all agreed the Func and Action are meaningless. No matter: Func<int, int> has no bearing on my delegate int Cycle(int).

You are saying that having structural function types is ok, because you can still have nominal function types. Tat's the same here. If you want nominal types, you can still have them. Classes/structs/intefaces are not going away. But if your domain is better suited to structural types, then you can have them.

It's another tool in the box, but for some reason it departs fundementally from the other tools in the box, and I don't see any benefit to doing so.

For one thing: performance. That's a very large benefit.

@Pzixel and I don't like my computer guessing at what I want; I like knowing that I've told it what I want, so that when I (or someone else) gets it wrong it can tell us back.

Inference is a large part of C# and has been since 2.0 onwards. Many people use 'var' extensively so they don't have to provide the types for locals. 99.9%+ of all generic methods call i make do not supply type arguments. That's because the language sensibly figures this out and does things write while allowing me to write less.

Having the language figure this out, and not forcing you to do everything explicitly is a virtue here.

This concerns me. You've been adamantly railing against structural typing, even saying things like:

You are right, I certainly employ hyperbole far too liberally (it's really 2 layers of sarcasm, but that doesn't make it any less unhelpful in an internet forum); but I'm merely expressing my opinion, not trying to frighten or convince anyone. I don't have much experience because what experience I have had I did not like (I tend to avoid things I don't like), and nominal typing is the only system I'm aware of that fits my mental model. I don't believe there are right answers, and if there were I'm not so daft as to think I'd have them, but I'm not a nominal-nutcase because it's all I know. I'm sorry that I'm so bad at expressing myself in text (not that I'm much better in voice), it's really not helpful for anyone, and it always ends up being very stressful for me.

This feels like a very hypothetical concern on your part as opposed to a true issue that happens in practice

Again you are right. I am (certainly (I'd hope) by the standards of this forum) an inexperienced developer, so my arguments are academic in nature because I don't have the breadth of experience to draw from, and don't wish to pretend that I do. None-the-less, it is a frightening prospect for me that the compiler will allow code to be used for a purpose it wasn't explicitly intended, which is of course subjective, but I'm not trying to suggest everyone should feel like I do, I'm just trying (and it seems failing) to convey why I have developed the opinions I hold since people have asked.

For one thing: performance. That's a very large benefit.

Now this one isn't just me being a sarcastic and miserable human being!

As far as I can tell, there is no performance benefit attained by this proposal that requires it be structural: that is down to the decisions made by the CLR folks, and how this proposal exploits specialisation for structural generics (another thing I adore about C#).

You are saying that having structural function types is ok, because you can still have nominal function types. Tat's the same here. If you want nominal types, you can still have them. Classes/structs/intefaces are not going away. But if your domain is better suited to structural types, then you can have them.

Clearly I've still not made that point clearly. There is nothing that compares to shapes/concepts in the language already (i.e. that provides a clean syntax to achieve this post-hocness). There is no nominal alternative. Even if there was, the fact that I could write classes and interfaces and nominal-shapes does not mean that structural-shapes will never influence my decision making; rather, their existence will influence decisions I make when I write my classes and interfaces.

The 'structural' function types are just intently meaningless nominal types: you can't cast something as an Action or a Func implicitly (excepting lambdas, which don't have a meaningful type). This proposal means you can treat a class or a struct as a MyConceptNameHere implicitly; it is a completely different arrangement.

Inference is a large part of C# and has been since 2.0 onwards. Many people use 'var' extensively so they don't have to provide the types for locals. 99.9%+ of all generic methods call I make do not supply type arguments.

Indeed, and I exploit both those features all the time; however, I do not think it a useful comparison because they are not part of a public API (they are a private concern for implementers), and they don't reach inside types (they just piggyback off the nominal type info available). var is completely innocuous, though I'll grant that issues with generics do stem from overloading (I shan't share my opinions on that). Some other stuff I though might have been problematic with generic-type-parameter inference doesn't seem to be the case, because the compiler doesn't try too hard for its own good (again, phrasing things emotionally, please forgive me).

As far as I can tell, there is no performance benefit attained by this proposal that requires it be structural: that is down to the decisions made by the CLR folks,

Yes. And that's a very big deal. The runtime changes almost not at all. We're about to get just about hte only single change to the runtime since 2.0. That's a virtue. The runtime acts as a large and stable platform that can be depended on to remain relatively constant for long swaths of time. The language then figures out great ways for people to be productive on top of that platform.

and I don't see any benefit to doing so.

So, when you say there is no benefit, and then disregard the benefit because of its necessity with how the platform works, that weakens the argument. The reality of hte situation is that the runtime is not going to change here. If it were, that would be probably 10 years down the line. In the meantime, people still want expressiveness, and would like it not at the cost of performance. And these sorts of approaches provide that. That is, unquestionably, a benefit.

You may not like that this is the balance that the runtime and languages have taken. That's fine. But it's the reality of hte situation.

Again you are right. I am (certainly (I'd hope) by the standards of this forum) an inexperienced developer, so my arguments are academic in nature because I don't have the breadth of experience to draw from, and don't wish to pretend that I do. None-the-less, it is a frightening prospect for me

This concerns me. You are allowing your own admitted lack of experience to frighten you. Might i suggest an alternate approach going forward? Instead of being frightened by the different and new, consider using these as opportunities to grow and better understand the value these systems provide and why many people find them just as good, if not better, than the alternates.

As i mentioned before, C# is unabashedly "jack of all trades". The language has evolved and thrived over the years precisely because it has been willing to look at hte rest of the world and say "that's actually really nice and would def make many types of development problems easier". The job has then become figuring out the best way to try to integrate that into what is already a large language in a way that feels great, while also being implemented on a runtime with very interesting characteristics.

their existence will influence decisions I make when I write my classes and interfaces.

Yes. Just like the existence of delegates influences my decisions wrt using an interface. The same with abstract classes vs interfaces. I have to make these decisions daily. But the presence of interfaces does not mean i don't use abstract classes (or vice versa). It means i pick the right tool for the job.

Indeed, and I exploit both those features all the time; however, I do not think it a useful comparison because they are not part of a public API (they are a private concern for implementers), and they don't reach inside types (they just piggyback off the nominal type info available)

But classes/structs/interfaces/delegates are part of your public API. And they do force people to think about the right tool for the job. Just because they're all nominal does not mean they don't make you think about a whole host of other factors. That's the job of hte API author in the first place. To pick from teh tools to provide the best solution for their domain. This just introduces a new tool. It will be the right choice in some domains, but will not be in others.

his proposal means you can treat a class or a struct as a MyConceptNameHere implicitly; it is a completely different arrangement.

And a delegate is a completely different arrangement from an interface. They're all 'completley different arrangments' because they are all different things. So yes, a shape wuold be different from a non-shape. That's the point. If it was not different, why would you ever use it?

Now, as you've said, the way that they're different is in a way that you have yourself admitted seems to cause you stress for some reason (even though no actual real-world problems seem to be present). So yes, you do not like this new and different thing because how it is different is precisely what you do not like.

--

However, this approach is not really a good one for language design. Given anything new, you will have some person that likely doesn't like it because they personally stress over that specific aspect. The real question is: is this a real-world problem, and not just someone's visceral dislike over something they don't have much experience with?

Note: this is not hypothetical. We get this with every language release. You would not believe how some people were against generics (using similar arguments to you), or lambdas, or dynamic, etc. In hte end, these all ended up being very valuable parts of hte language that people now take for granted as great tools to build libraries and codebases out of.

You are right, I certainly employ hyperbole far too liberally (it's really 2 layers of sarcasm, but that doesn't make it any less unhelpful in an internet forum);

I actually do think it's unhelpful. If this is actually a bad language feature we should be able to point to real cases that help justify that position. That happens all the time. Many language features are proposed and real examples of how that would make code worse are provided all the time.

You've been adamant that this will be bad for certain reasons (like people making more mistakes). But i still haven't seen a good justification for those positions. As such, this approach is actively unhelpful. I'm very open to hearing real criticisms and real world concerns. They're usually the #1 thing that i use when deciding if something is a bad idea or not. But if all i'm seeing is a vehement and passionate tirade against a feature, without actual real cases to back things up, then i'm left actually unchanged from where i was in the beginning because none of the critique provided any fruit.

--

IMO, the best way for this conversation to proceed would be to:

  1. dial waaaaay back on the rhetoric, fear, hyperbole and sarcasm.
  2. focus much less on what you "feel" and more on what sort of pros cons come directly from this feature on real code cases.

That will keep things well focused and will help ensure that if htere are problems, they are rightly considered when determining if this feature is an overall positive or not.