It's like Deja-vu
Many months ago I wrote a blog discussing an issue we were having with the Implement-Interface smart tag concerning what we should do when we cannot perform an action that the user has requested. Well (like most unresolved issues) the topic has reared it's ugly head and we are faced with a couple more confusing situations.
The first problem is most evident due a recent change in the BCL concerning our core collections classes. Basically at the root of everything are the following two classes:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerable<T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
(the recent change was to make IEnumerable<T> implement IEnumerable). So why is this an issue? Well imagine you have typed the following:
public class MyCollection<T> : IEnumerable<T>
{
}
and you then ask us to implement the interface implicitly. Well, as it turns out we can't implement that interface implicitly. If we did you'd end up with
public class MyCollection<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator()
{
throw new Exception("The method or operation is not implemented.");
}
public IEnumerator GetEnumerator()
{
throw new Exception("The method or operation is not implemented.");
}
}
which is illegal C# code as you are not allowed to have two methods with the same name as same parameter types. So how would a user normally resolve this where two interfaces conflict with eachother? In C# you would use explicit interface implementation and you would write the second method as:
IEnumerator IEnumerable.GetEnumerator()
{
throw new Exception("The method or operation is not implemented.");
}
So what do we do about it. Right now we're falling back to implementing one of the methods explicitly so that we don't produce broken code. In this case we're leaning toward that being more important than strictly following the letter of what was stated earlier in the smart tag. Other options that we could have done would be to produce the broken code (not very nice since you have to fix it up), pop up a message telling you that we did that (with all of the associated burden of more UI annoying you), or disabling the option to implement the interface implicitly (with all the associated confusion from the user as to why it was no longer an option).
The second problem is a little more subtle, and it's definitely not clear what we should be doing. Say you have the following code:
public interface IFoo
{
void DoIt();
}
public class Base
{
public void DoIt() { }
}
public class Derived : Base
, IFoo
{
}
and you ask to implicitly implement IFoo. Currently our behavior is to dump the method in the 'Derived' class. Part of me thinks that this is broken behavior. 'Derived' already implements 'IFoo' through Base::DoIt, so it doesn't need any extra implementation. On the other hand, sometimes people actually want 'Derived' to reimplement the IFoo interface by hiding the member in the base class (by using the 'new' keyword). Consider the example above, but this time you have a fully fleshed out IFoo interface with many methods and 'Base' implements just one of them. A user might be very suprised after implementing all of IFoo's methods that the DoIt method is actually calling into the 'Base' class instead of the class they thought they were implementing it on.
A common pattern that occurs in my code is to have a deeply nested object hierarchy that mimics a deeply nested interface hierarchy. So you have something like:
public interface IEnumerable { /*...*/ }
public interface IIterable : IEnumerable { /*...*/ }
public interface ICollection : IIterable { /*...*/ }
public interface IMap : ICollection { /*...*/ }
public interface ISortedMap : IMap { /*...*/ }
public interface IList : ISortedMap { /*...*/ }
public class ArrayEnumerable : IEnumerable { /*...*/ }
public class ArrayIterable : ArrayEnumerable, IIterable { /*...*/ }
public class ArrayCollection : ArrayIterable, ICollection { /*...*/ }
public class ArrayMap : ArrayCollection, IMap { /*...*/ }
public class ArraySortedMap : ArrayMap, ISortedMap { /*...*/ }
public class ArrayList : ArraySortedMap, IList { /*...*/ }
So, for me if i'm asking to implement "IList" on "ArrayList" i definitely do not want stubs for every interface in that hierarchy generated. They've already been implemented so why would i want to implement them again? All that happens is I end up with massive code spew that i then have to clean up afterwards.
So I'm changing the behavior slightly (and adding some configurability) and i wanted to get your thoughts on it. Here are the rules now:
- If you're implementing an interface member 'implicitly'
- If your type contains an implicit member that matches the interface member then we don't generate it
- If the option "check super types for matching members" is set and one of your super types contains an implicit member that matches the interface member then we don't generate it
- If one of your super types contains an implicit member that conflicts with the interface member then we generate it with the "new" keyword to indicate that hiding is occurring
- If one of the methods in your type conflicts with the interface then we explicitly implement i
- If you're implementing an interface member 'explicitly'
- If your type contains an explicit member that matches the interface member then we don't generate it
- If the option "check super types for matching members" is set and one of your super types contains an explicit member that matches the interface member then we don't generate it
By default we will be set to check super types when you're implementing implicitly and to not check super types when you're implementing explicitly.
To me this fits best with the model of "implement interface simply does the minimal work necessary to get your code actually implementing the interfaces it declares." However, if you feel that you're a person who wants this feature to stub out the entire interface and then you prefer to remove the possible unnecessary stubs, then there are options to all you to go back to that behavior.
How do you feel about that? Are we being totally boneheaded here? Is there an easy way to do the right thing and have everyone understand what's going on?
Comments
- Anonymous
November 17, 2004
I have a definite opinion on interface implimitations. Why can't a method with a more specific return type match an implementation? This would solve your first problem.
For example...
interface ISimple
object GetObject();
class Simple : ISimple
string GetObject();
Simple's method GetObject should satisfy the ISimple contract. However, the current version of C# doesn't agree and gives a compile error saying you haven't implemented a method GetObject() that returns an object. But string is an object!
If C# would accept this, then it solves your 1st problem. IEnumerator<T> is a sub-type of IEnumerator. So you don't have to implement GetEnumerator twice. One method returning IEnumerator<T> satisfies the contract for IEnumerable.
So I guess the question becomes, why doesn't C# accept a method whose return type is a sub-type. Let's consider this example...
class A
virtual int GetValue() { ... }
class B : A
override int GetValue() { return 214; }
Now B has implemented its own version of GetValue. And its dramtically limited the scope of what values can be returned from that method. Its domain of possible values is acutally just one value, 214.
So obviously a sub-class can re-implement a method and limit the domain of possible values that method will return. But then why can't it change the return type of the method to reflect this? I'm not sure if uint is a sub-type of int, but let's say that it is (logically POSITIVE_INTEGER is a sub-type of INTEGER). Why can't B implement the method with a unit since it knows that it will never return a negative number.
class B : A
override uint GetValue() { return 214; }
This should not be considered a new method. Its the same method because uint is an int. If I had a collection of A's and called GetValue on each, I would expect an int back. And if some of those A's happened to B's (because B is a sub-type of A) and they returned a uint, well that's still fine because uint IS an int. I have not violated A's contract in anyway.
So my question is, what does C# not allow this. Will it change in C# 2.0? - Anonymous
November 17, 2004
The comment has been removed - Anonymous
November 17, 2004
I found myself re-reading 1.3 and 2.2 a couple of times, but I like "minimal work necessary" approach personally. Thanks for asking and caring.
Also, I thought Covariance was supported...or is that only for delegate return values? - Anonymous
November 17, 2004
Sean: Yup. Only for delegates. - Anonymous
November 17, 2004
The comment has been removed - Anonymous
November 17, 2004
"This should not be considered a new method. Its the same method because uint is an int."
It certainly shouldn't be.
uint and int are both narrower than long, so if the base class declared the return type as long, methods returning int and uint should both be reasonable overrides (as they're narrowing the return type, which is safe).
If the base class declared the argument type as short, a method taking int (or long) as an argument should also be a reasonable override (as they're both wider than the argument type, which is safe).
The domain of int and uint both fit into long, but they don't fit into each other. So to claim that uint is an int seems rather silly to me. It might have the same size, and some numbers may have the same representation, but they're not identical. - Anonymous
November 17, 2004
Cyrus,
I think your approach is the appropriate action (generally), but I'm left wondering about implementing interfaces with partially overlapping methods. For example:
public interface IFoo
{
..void Initialize(object value);
..object GetValue();
}
public interface IBar : IFoo
{
..new int GetValue();
}
Would implicit interface implementation be like so:
public class MyValue : IBar
{
..public int GetValue()
..{
....throw new Exception(...);
..}
..public void Initialize(object value)
..{
....throw new Exception(...);
..}
..public object IFoo.GetValue()
..{
....throw new Exception(...);
..}
}
If so, is there a potential point of confusion here? Does it matter if a subset of members is explicitly implemented due to conflicts? - Anonymous
November 17, 2004
A picky point about consistency:
In your examples of the code generated by "implement interface" you show that the methods throw Exception("The method or operation ..."). Is that actually what you generate. If so, shouldn't it be throw NotImplementedException("...")? - Anonymous
November 18, 2004
The comment has been removed - Anonymous
November 18, 2004
Samuel Jack: "NotImplementedException" is not available in the compact .net framework 1.0.
We had to make a decision about the best possible exception versus the fact that the code we'd be generating wouldn't compile on one of our larger platforms.
Because tehy're just .snippet files you can (and we'd like you to) edit them to be appropriate for your needs.
We're still trying to figure out if there's a way to support both scenarios OOB simply.
(good catch btw). - Anonymous
November 18, 2004
Note, the "Object+" syntax is just a throwaway syntax for teh example. I got a good chance to talk to one of hte greators of the java1.5 type system (who also agrees taht not changing the VM was a bad idea) and about how we could add variance into our type system safely.
The only problem is how to do it in a way that does freak out every C# user.
So I'm hoping we do generics, give people a change to adjust to them, then enhance them based on community demand. - Anonymous
November 18, 2004
I know this is a tangent from the real issue, but...
Not only are the internal representations of uint and int different, but you really can't say that every valid uint value could be represented by an int.
Consider:
class B : A
override uint GetValue() { return 2147483648; }
It returns a valid uint, but not a valid int. - Anonymous
November 18, 2004
How do covariant return types/contravariant argument types have any influence on the type system?
It's not clear what extra magic they might need. Particularly not in something like C# where overrides are explicitly declared as such. - Anonymous
November 18, 2004
"What's worse is that we don't have any sort of variance system for generics. So there is no way to do something like.
IList<Object+> = new ArrayList<string>(); "
I don't see why there should be such a capability. A list-of-string is not a list-of-object. List-of-object has a method equivalent to put(Object o). List-of-string does not. List-of-string should not be in_any_way convertible to list-of-object short of creating a new list-of-object populated from the list-of-string.
The problem is that the list interface has the element type as both an argument (for 'put' actions) and a return type (for 'get' actions). That means no subtyping for you. What you've written is fine for the 'get' portion (making the return type more restrictive is fine), but not for the 'put' portion; the reverse (IList<string> = new ArrayList<object>()) is fine for the 'put' portion (making the argument less restrictive is fine), but not for the get portion.
Allowing covariant arguments and contravariant return types is frankly wrong; I think Eiffel permits it and inserts some runtime checks to stop things from breaking too badly. - Anonymous
November 18, 2004
The comment has been removed - Anonymous
November 18, 2004
DrPizza: This is nice because it takes the onus of writing a Base interface with no mutators and a base interface with no accessors off of the library writers hand. Less chance for error, and more flexibility for the user of C# and the BCL than the current generics implementation. - Anonymous
November 18, 2004
The comment has been removed - Anonymous
November 18, 2004
I know what covariant is, but I have never seen contravariant before. Googling for it sends me into the math world of vectors, which is probably not closely related.
Could you enlighten me?
Thanks - Anonymous
November 18, 2004
My example of uint/int was bad because as some people have pointed out, uint IS NOT a sub-type of int. Logically, PositiveInteger would be a subset of Integer, but uint and int are not true domain classes since they have some physical limitations on their size. So uint cannot inherit from int.
This concept of covariance and contravariance only applies to types that inherit from each other.
And I take it that covariance referrers to argument variance, where a super-class defines a method with a specific argument like MemoryStream but a sub-class overrides it and redefines the argument to something more general like Stream.
And it sounds like contravariance applies to return types, where the super-class returns something general (Stream) but a sub-class overrides it and returns something more specific (MemoryStream). - Anonymous
November 18, 2004
@DrPizza
"Allowing covariant arguments and contravariant return types is frankly wrong"
Why is it wrong? - Anonymous
November 19, 2004
"Why is it wrong? "
'cos you're no longer overriding, because you're no longer accepting compatible arguments. - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
"public class Derived : IFoo" should be "public class Derived : Base, IFoo" right? - Anonymous
November 19, 2004
Mattias: Yup. Fixed. Thanks for the catch. - Anonymous
November 19, 2004
"Now, the easiest way to do that would be to force you to provide an ICollection<string>. But why is that necessary? All i'm doing is providing you with the data and in your case an IList<object> would suffice for storing those strings. However, it's not possible to pass an IList<object> to a method needing an ICollection<string>. Of course, i could relax my method and have it take an ICollecction<object>, but then if you actually had an IList<string> then you still wouldn't be able to pass that in to me. The problem is that all i need you to pass me is a general container that can hold anything, i.e. an ICollection<string->. "
So don't specify what type I pass you. Just specify that you want a container of T, and let me specify what T is when I pass you the container.
"Your example with managed C++ still won't work. "
There's no reason it can't work, and it would work without breaking subtyping, unlike your proposal.
"If I have a method that looks like:
AddMyStuffToYourCollection<T>(ICollection<T> c) {
....c.AddAll(myStuff); //<-- not allowed. AddAll needs to make sure that myStuff is a container of T or things derived from T. We cannot guarantee that.
} "
I don't see what you mean by "not allowed". Its allowableness will depend on what AddAll is. AddAll ought to take a ICollection<U> where U is constrained by deriving or being T.
I can do this in C++, so why can't it be done in .NET?
"No, the runtime matches on the entire signature."
Why? Does it permit something interesting as a result of this?
"The difference being that in the first list i know that the list contains objects, but i don't know exactly what the type is."
But you don't know exactly what the type is in the second either. Just because you've got an IList<object> doesn't mean that they're all actually objects.
"If you're familiar with existential types then it's equiv to:
IList<object+> === There Exists X such that IList<X>
IList<string+> === There exists X such that IList<X> and X isa string.
etc. "
But there exists no such X for any type other than string.
IList<object+> === There Exists X such that IList<X>
IList<string+> === There exists X such that IList<X> and X isa string.
etc.
Our IList looks something like:
interface IList<T>
{
T get();
void put(T t);
}
class List<T> : IList<T>
{
T get();
void put(T t);
}
And let's have a little hierarchy:
struct B {}
struct D : B {}
So you want to be able to have
IList<B+> b1 = new List<D>;
D1 is-a B, so that should be a good enough match for B+.
B is also a B, so we can also go:
IList<B+> b2 = new List<B>;
But this is now no good.
I can do this:
b2.add(new B());
but I can't do this:
b1.add(new B());
So why are you making them subtype?
"IList<string-> === There exists X such that IList<X> and string isa X. "
The same happens here.
"Similarly if i am an ICollection<string> i am an ICollection<object+>, but that doesn't let Add(new Object()), because the type signatures don't match."
But they do match, because object is-a object, so is compatible with object+.
"You're right. I shouldn't have tried to simplify it in that manner. It is an IList and it can do everything that an IList can do. And, as long as you satisfy the type restrictions you willbe able to do that. If you can't, then the compiler will error and tell you right away (just like with regular generics). "
Those type restrictions make it not an IList. It no longer does what an IList can do. It's not substitutable.
"We don't need to pretend anything. These things are what they are. They just use existential types. And as usual with those types, you must supply a proof (in the form of constraints) to show what the set of types are that are valid in that situation. This is far more fflexible, because as i said it no longer requires the user to prove that they can have objects added to them when all i want them to do is prove that they can give objects back to me."
So write a function template and they don't have to. And they don't have to have ILists-that-aren't-ILists. An IList that returns something it can't insert isn't an IList any more, because the contract of IList says you can do that.
"Normal generics provide a large onus on the consumer to make the types exactly match what is specified even if it it isn't necessary for their object to be exactly of that type."
Rubbish.
"No, the inheritance hierarchy already exists."
Except that they're not subtypes so don't belong in an inheritance hierarchy.
"Because they don't need the functionality of a full list, and by forcing users to provide items that match that full functionality they put a large burden on the library consumer that has a perfectly valid alternate item to use. "
But users aren't forced to do any such thing.
"Because it often won't work and because it makes library consumption rather difficult. "
It works in C++ and it doesn't make library consumption difficult. - Anonymous
November 19, 2004
Dr.Pizza: "So don't specify what type I pass you. Just specify that you want a container of T, and let me specify what T is when I pass you the container. "
This won't work. Try to write it. if i make my method parameterized on T then i will not be able to add strings to it. - Anonymous
November 19, 2004
DrPizza: "I don't see what you mean by "not allowed". Its allowableness will depend on what AddAll is. AddAll ought to take a ICollection<U> where U is constrained by deriving or being T. "
Exactly! So how do i add a string to that? We just said that it's an ICollection<U> where U is derived from T. But neither T nor U make the gurantee that this collection can hold strings. - Anonymous
November 19, 2004
DrPizza: ""No, the runtime matches on the entire signature."
Why? Does it permit something interesting as a result of this? "
No, it doesn't. But that's how it's implemented. And, as i've said over and over again, we cannot do covariant return types without this support. That's all i'm saying. If the runtime relaxes this restriction in teh future then we will be able to do it. But, that requires both them and us to do work. - Anonymous
November 19, 2004
DrPizza:
"
IList<B+> b1 = new List<D>;
D1 is-a B, so that should be a good enough match for B+.
B is also a B, so we can also go:
IList<B+> b2 = new List<B>;
But this is now no good.
I can do this:
b2.add(new B());
but I can't do this:
b1.add(new B()); "
No, you can do neither. All you have done is declare that there is some type (that is at least a B) that these lists were instantiated with. You have not stated that that type actually is B. so you will not be allowed to add anything to these lists. - Anonymous
November 19, 2004
DrPizza:
"Similarly if i am an ICollection<string> i am an ICollection<object+>, but that doesn't let Add(new Object()), because the type signatures don't match."
But they do match, because object is-a object, so is compatible with object+. "
No. ICollection<object+> says "i am a collection of some type, where that type is at least an object, but not necessarily object itself.
i.e. i'm a collection of ints (ints are object), i'm a collection of strings (string are object), i'm a collection of objects (objects are objects). But i don't know which one i am. So adding an object to me is not supported because you haven't supplied proof that i was actually instantiated as IList<object> - Anonymous
November 19, 2004
DrPizza:
""We don't need to pretend anything. These things are what they are. They just use existential types. And as usual with those types, you must supply a proof (in the form of constraints) to show what the set of types are that are valid in that situation. This is far more fflexible, because as i said it no longer requires the user to prove that they can have objects added to them when all i want them to do is prove that they can give objects back to me."
So write a function template and they don't have to. And they don't have to have ILists-that-aren't-ILists. An IList that returns something it can't insert isn't an IList any more, because the contract of IList says you can do that. "
a) We don't have templates in C#
b) Where does IList ever state that it can insert something it returns?
IList states that it can add something that is known to match the type the list was instantiated with. If you don't know what taht type is then you can't add something.
If i instantiate a list as:
IList<string-> then i know that this was instantiated as either IList<string> or IList<object> and its' safe to add either of those types to this list. - Anonymous
November 19, 2004
DrPizza:
""Normal generics provide a large onus on the consumer to make the types exactly match what is specified even if it it isn't necessary for their object to be exactly of that type."
Rubbish. "
Explain. I've already given examples of where this arises. You have not shown how to do it otherwise with generics. "Rubbish" isn't an argument.
-------
"No, the inheritance hierarchy already exists."
Except that they're not subtypes so don't belong in an inheritance hierarchy. "
Yes, they are subtypes and there are many proofs showing them to be such.
----
"Because they don't need the functionality of a full list, and by forcing users to provide items that match that full functionality they put a large burden on the library consumer that has a perfectly valid alternate item to use. "
But users aren't forced to do any such thing. "
Yes they are. There is no way for me (as a user) to do much of what i've already mentioned. If someone takes an ICollection<string> with the intent of only placing strings in it I cannot provide any ICollection<object> even though that is a perfectly fine container for the elements this person is providing.
-----
"Because it often won't work and because it makes library consumption rather difficult. "
It works in C++ and it doesn't make library consumption difficult. "
C++ has templates. I already said taht. These are not templates and it's incorrect to think of them as such.
Generics in .net are weaker than templates (by design). allowing for this type-safe inheritance to be exposed through teh language is one way of making of for some (But obviously not all) of the limitations. Without a full functional language at our disposal at compile time we can only prove things like this by adding them to the type system instead of realying on C++ to check things like structural subtyping and matching (concepts which have no analogue in C#). - Anonymous
November 19, 2004
"This won't work. Try to write it. if i make my method parameterized on T then i will not be able to add strings to it. "
You seem to jump between what the system does let you do and what you want it to let you do, but you don't appear to afford me the same privilege.
It is possible to construct a system where this works in the way I describe; such systems even exist.
"Exactly! So how do i add a string to that? We just said that it's an ICollection<U> where U is derived from T. But neither T nor U make the gurantee that this collection can hold strings. "
Likewise.
"No, it doesn't. But that's how it's implemented. And, as i've said over and over again, we cannot do covariant return types without this support. That's all i'm saying. If the runtime relaxes this restriction in teh future then we will be able to do it. But, that requires both them and us to do work. "
Why did they make it match signatures in such a way, then? Is there some benefit to doing it? Does the runtime permit overloading by return type or something? Hrm, I suppose it can't, or else you could use a similar approach to the contravariant argument workaround.
"b) Where does IList ever state that it can insert something it returns? "
T this[int index] { get; set; }
"IList states that it can add something that is known to match the type the list was instantiated with."
It does more than that. It also says that the things it returns match the type the list was instantiated with, because we have:
T this[int index] { get; set; }
Even if I don't know what T is I do know that it's both the return type and argument type of the indexer. I don't care what it is--they necessarily match.
"If you don't know what taht type is then you can't add something. "
You don't have to know what it is; just that it's been returned by IList. And I can in any case find out the type, by asking the runtime.
"Explain. I've already given examples of where this arises."
Not on this page you haven't.
"Yes, they are subtypes and there are many proofs showing them to be such. "
How can they be shown to be such when these "subtypes" have different interfaces (losing, as they do, one method or the other due to contravariant return types and covariant argument types)?
"Yes they are. There is no way for me (as a user) to do much of what i've already mentioned. If someone takes an ICollection<string> with the intent of only placing strings in it I cannot provide any ICollection<object> even though that is a perfectly fine container for the elements this person is providing. "
Why.
Are.
They.
Taking.
An.
ICollection<string>.
If.
That.
Is.
Not.
What.
They.
Really.
Want.
I mean, seriously. What is your point? If I only want to place strings in a container but don't care if the container is homogeneous WHY WOULD I SPECIFY THAT THE CONTAINER MUST CONTAIN STRINGS? It's like complaining that a method that takes a string argument can't take arbitrary objects as its argument. No, of course it can't so don't do that. Make it take an object.
"Generics in .net are weaker than templates (by design)."
More "different" than "weaker" I would think. They're closer in capabilities to C's qsort (with its void*s and function pointers, allowing the same function to compare different types selected at runtime) with some added typechecking (and complete ignorance of LSP) than templates. Different set of trade-offs. Generics, of course, allow simple separate compilation, which templates lack.
"Without a full functional language at our disposal at compile time we can only prove things like this by adding them to the type system instead of realying on C++ to check things like structural subtyping and matching (concepts which have no analogue in C#). "
Well that's hardly my fault. - Anonymous
November 19, 2004
DrPizza: "Why.
Are.
They.
Taking.
An.
ICollection<string>.
If.
That.
Is.
Not.
What.
They.
Really.
Want. "
Because there is nothing else for them to take...
They want a collection to put strings into. What are the options? ICollection<string> or ICollection<object>. Both put restrictions on the user as to what htey can pass in. The former prevents them from passing in an ICollection<object>, the latter an ICollection<string>. However, both would be acceptable for the task. - Anonymous
November 19, 2004
DrPizza: ""This won't work. Try to write it. if i make my method parameterized on T then i will not be able to add strings to it. "
You seem to jump between what the system does let you do and what you want it to let you do, but you don't appear to afford me the same privilege.
It is possible to construct a system where this works in the way I describe; such systems even exist. "
How do you construct such a system with the current generics in C#? - Anonymous
November 19, 2004
DrPizza: ""b) Where does IList ever state that it can insert something it returns? "
T this[int index] { get; set; }
"IList states that it can add something that is known to match the type the list was instantiated with."
It does more than that. It also says that the things it returns match the type the list was instantiated with, because we have:
T this[int index] { get; set; } "
Yes, and that still holds in the variant case.
However, if instantiate an IList<T> as IList<object+> then i have:
object+ this[int index] { get; set; }
then what we have in an IList instantiated such that a proof has been provided that it will always return an object from it's indexer's getter, but no such proof has been given about what the indexer's setter can take. This is a fundamental property of existential types.
Now, for users who don't want the burden of proving that their list can accept objects, then this is a perfect scenario to use variance to get around that issue. Likewise for users (commonly library writers) who only want generalized containers to store things into, then variance also allows this.
Without variance the library writer and library consumer must agree on the exact instantiated type Even though all that gets them is the ability to call methods that they don't even care about.
variance loosens that restriction on them and allows people to use a wider set of types which might then be more useful to the task at hand. - Anonymous
November 19, 2004
DrPizza: ""Explain. I've already given examples of where this arises."
Not on this page you haven't. "
Yes i have. I've given two examples. One from a production standpoint, one from a consumption standpoint. You've said they aren't necessary but haven't shown code that will do the equivlent. Youv'e givn examples of how to do it in C++ with templates, but those capabilities are not available to us with generics. - Anonymous
November 19, 2004
DrPizza: "I mean, seriously. What is your point? If I only want to place strings in a container but don't care if the container is homogeneous WHY WOULD I SPECIFY THAT THE CONTAINER MUST CONTAIN STRINGS? It's like complaining that a method that takes a string argument can't take arbitrary objects as its argument. No, of course it can't so don't do that. Make it take an object. "
Exactly.
Now, say you've written it is:
SomeMethod(ICollection<object> collection).
now the user cannot pass in one of their ICollection<string>'s even though all i am going to do is add strings into it.
---
Once again, how would you go about doing this? I need to put strings into a container you pass me. Why should i restrict you to just ICollection<object>'s or ICollection<string>'s when either woudl suffice? - Anonymous
November 19, 2004
DrPizza: ""Without a full functional language at our disposal at compile time we can only prove things like this by adding them to the type system instead of realying on C++ to check things like structural subtyping and matching (concepts which have no analogue in C#). "
Well that's hardly my fault. "
Who said anything about fault? You've been stating that we go about this disparaity by using the structral matching that C++ uses in templates. However, we cannot use them as they are purely a compile time mechanism.
We are using .net generics here and as such these variance ideas would simply be exposing more of the typing system that already exists but is currently hidden from you. Regardless of how you feel about this, if T : U then IList<T+> : IList<U+> . That's true no matter what (whereas IList<T> : IList<U> is definitely not true). However, in the current implementation of generics we do expose this and so we cause pain for anyone who is writing code that really only needs the generics to be co/contravariant.
As it turns out, we've run into a similar problem when designing a class hierarchy for our symbol table. When using C++'s templates we are able to simulate covariant generics (since C++ just does structural matching and see's that it's ok since we don't call any setters). However, when we try to reimplement this in C# generics we are stopped (Both by the lack of covariant return types and the lack of variant generics). So, a disgn pattern that was very intuitive and implementable with templates is no longer doable with generics.
However relaxing the language (And runtime) to support covariant return types and variance would allow the same things to be done taht we do in C++ without any loss of expressibility or type safety. - Anonymous
November 19, 2004
"then what we have in an IList instantiated such that a proof has been provided that it will always return an object from it's indexer's getter, but no such proof has been given about what the indexer's setter can take."
They take the same type. It says so right there. It says that what the getter gets the setter can set. That's why they, you know, use the same type.
Do you know what type the getter is actually getting? No (well, not withot asking). But that doesn't matter; it's the same type as the setter is expecting. object+. - Anonymous
November 19, 2004
"Who said anything about fault? You've been stating that we go about this disparaity by using the structral matching that C++ uses in templates. However, we cannot use them as they are purely a compile time mechanism. "
But that wasn't my decision, so why are you making me stick with it? I'm still not convinced that it was a good decision. Sure, separate compilation is nice. But is it really that nice? It's genuinely not clear to me that it is.
And this assumes that a template model with separate compilation is impossible, and I'm not sure that that's actually true. - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
DrPizza: ""Who said anything about fault? You've been stating that we go about this disparaity by using the structral matching that C++ uses in templates. However, we cannot use them as they are purely a compile time mechanism. "
But that wasn't my decision, so why are you making me stick with it?"
No one is making you stick with it. You can use whatever language you want. However, C# does not have templates and probably will not be getting them. We instead went with generics.
----
"I'm still not convinced that it was a good decision. Sure, separate compilation is nice. But is it really that nice? It's genuinely not clear to me that it is. "
I'm not convinced that it was a good decision either. However, that decision cannot be undone at this point. So what we are discussing is changes to the language to enable similar functinoality to what has been lost in a safe way.
-------------
"And this assumes that a template model with separate compilation is impossible, and I'm not sure that that's actually true. "
Sorry, i should not have implied it was impossible.
But again, even if we were to move to a template based model (which allowed structural subtyping and would enable what's being discussed here), it would not change anything about the validity of the model currently being presented. - Anonymous
November 19, 2004
DrPizza: I don't think this discussion medium is well suited to conversing about this topic. I woudl appreciate it if you would continue it with me through the "contact" link. Through that we can discuss your concerns and things like proofs of the type system, existential types, recursive types, structural subtyping. IIt would also be interesting to talk about templates and the issues involved when you want to base your type system on them. I could then post a blog summarizing and explaining what we talked about.
Thanks! - Anonymous
November 19, 2004
The comment has been removed - Anonymous
November 19, 2004
DrPizza: "It makes the whole argument. They're both of type object+."
I'm not sure how to interpret your statement. It's too terse. As i mentioned before, we can discuss this through the contact link. I think that would be best since it does not appear we are making any headway here.
As it stands, you are correct. both are of type 'object+', which is exactly why you can't call the setter.
You haven't supplied enough information for the existential type to know how the type was actually instantiated as. The type could have been instantiated as IList<int> or any other type (since all types derive from object). Because you haven't supplied enough information (why i said "only 1/2 of the argument") the type system cannot verify that you can call the setter safely and will prevent you from doing so. However, if you supply enough information to limit (prove) what the type was actually instantiated as then it will be safe to call with an argument of that type. - Anonymous
November 21, 2004
"As it stands, you are correct. both are of type 'object+', which is exactly why you can't call the setter. "
But the setter says that I can call it with type object+. So why are you claiming I can't?
"You haven't supplied enough information for the existential type to know how the type was actually instantiated as."
But I don't need to specify any more information. The indexer says that the return type and the argument type are one and the same. Whatever proving needs to be made has already been done. I don't need to know how_the_type_was_actually_instantiated, because that doesn't change the interface. All I need to know is that you can set what you get. And you can, because the interface tells you so. - Anonymous
November 21, 2004
"(the recent change was to make IEnumerable<T> implement IEnumerable)."
Even though it doesn't? - Anonymous
November 21, 2004
Very late to the party here - interesting discussion above in which I think you guys need to start over. You both appear to be arguing about 10 unrelated points in a mixed fashion. But largely I think DrPizza is closer to my thinking here. Nonetheless I got a bit lost off after a while!
Anyway, getting back to the original point.
The only real answer IMHO is to explicitly implement both methods. This will ensure the code works and that it is clear what has happened - the important stuff. If the user then chooses to make one of them implicit that's a simple change.
Of course I have no idea why you'd want to write
[code]public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerable<T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}[/code]
Surely that is not a correct thing to do? Surely what you really meant was:
[code]public interface IEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerable : IEnumerable<object>
{
}[/code]
was it not?
On the second point, I would expect:
"'Derived' already implements 'IFoo' through Base::DoIt, so it doesn't need any extra implementation."
but if you arn't happy with that then your only real alternative is to implement both methods explicitly:
[code]
public class Derived: Base, IFoo
{
public void IFoo.DoIt() {throw new NotImplementedException(); }
public void Base.DoIt() { base.DoIt(); }
}[/code]
And allow the user to pick the one they want implicit by simply deleteing a few characters.
Finally,
IList<Object+> = new ArrayList<string>();
that strikes me as just plain wrong. But alas I can't put my finger on exactly why.
Object a = new String(); is fine, and we know that we must cast to do:
String b = (String)a;
so it would seem that
IList<object> a = new ArrayList<string>(); would also be fine.
However I don't feel it is. I think that comes from the fact that I can derive no valid interpretation for
(IList<object>)new ArrayList<string>()
- is there one? - Anonymous
November 21, 2004
"We had to make a decision about the best possible exception versus the fact that the code we'd be generating wouldn't compile on one of our larger platforms.
Because tehy're just .snippet files you can (and we'd like you to) edit them to be appropriate for your needs. "
--
This seems like an excellent thing for your proposed TweakC# utility to set.
In fact even if it was just a list of files that opened in notepad it would be a bonus to have a utility that lists all the studio snippet files and code templates (and the ability to reset them to default). I currently go through and edit them manually if I want to tweak them to my liking.
I always hoped the ASP.Net guys would something similar with the intellisense for Web Custom Controls with autogeneration of the intellisense Xml file (though at least there are some third party utils for this) still easy editing of the c# templates would make my day ;) - Anonymous
November 22, 2004
DrPizza: Please contact me through the links at the top of the page. This format is not conducive to having a good discussion about the type theory issues being raised here. - Anonymous
November 22, 2004
The comment has been removed - Anonymous
November 22, 2004
DrPizza: ""(the recent change was to make IEnumerable<T> implement IEnumerable)."
Even though it doesn't? "
I agree taht it doesn't. It was jsut used as an example of the complexity of "Implement Interface."
Any example where you have multiple interfaces with the same method name and arguments would have sufficed.
I'm sorry i brought up that example in this discussion as it would have been more interesting as a discussion on it own. This post however it concerning "Implement Interface" and how to deal with inferring a users' intention from teh code they've written. Personally, i think inference is terribly difficult, and any rules you come up with run the risk of:
a) not hitting all scenarios
b) being too complex for the user to understand
The alternative is to give the user the flexibility to decide the behavior. However, that can lead to complicated UI and interfering with a code focussed developer who jsut wants the tools to help him and not get in the way. - Anonymous
November 22, 2004
The comment has been removed - Anonymous
November 22, 2004
"DrPizza: Please contact me through the links at the top of the page. This format is not conducive to having a good discussion about the type theory issues being raised here. "
Can you not plumb in a forum-style backend?
That'd be much better than these blog comments. - Anonymous
September 01, 2005
Very nice blog.