-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
declare superinterfaces of existing types #1416
Comments
Potential problem: class C() {}
class D() {}
Set<C> cs = …;
Set<D> ds = …;
Set<Nothing> empty = cs & ds; // well-typed: C and D are disjoint
shared interface Foo
abstracts C & D {}
Set<Foo> empty2 = cs & ds;
Set<Nothing> empty3 = empty2; // well-typed? |
I think extension methods, as proposed by #1146, are a much better solution to this need. |
Well, if |
I think it would be cool to declare new members to existing types that can be overridden, which isn't the case in #1146... |
Another potential problem: // module a
shared interface Described abstracts Object {
shared default String description => string;
}
// module b
shared class Place(name, coordinates, description, rating) {
shared String name;
shared Coordinates coordinates;
shared String description;
shared Float rating;
}
// module c
module c "1.0.0" {
shared import a "1.0.0";
shared import b "1.0.0";
}
// error: non-actual member refines an inherited member: 'description' in 'Place' refines 'description' in 'Described' Modules |
No, those would be completely different members, that happen to have the same name. It could be disambiguated by explicitly importing one of the types, and renaming one of the problematic members. Sure, adding a abstracting interface to an already existent module could break modules that import it, but currently it isn't allowed for a module to "import the newest available version of another module", and things like that are the reason. Importing the newer module would either not cause problems, or fail at compile time, instead of compiling with a different behavior than expected... |
shared interface Foo
abstracts Iterable&Bar Could you explain in more detail what this means? I’m not sure why, in your example above, some lines in |
It means that |
But why |
Yeah... I'm just showing that you could use intersections if you wanted as well.. Unions feel obvious enough, so I decided to not bother mentioning it... But indeed, I struggled to think of the usefulness of having intersections there... =P |
Also, I can't agree with myself on what should be the behavior of having type parameters on those interfaces... I think it simply shouldn't be allowed... |
I have a slightly amended proposal: shared interface Super
abstracts Sub1 | Sub2 {
shared default String member => "";
} The Sub sub = …;
// Super sup = sub; // error: Sub is not a subtype of Super
value v1 = sub.member; // unambiguously refers to Sub.member
Super sup = sub of Super; // okay: Super covers Sub and Super is a subtype of Super
value v2 = (sub of Super).member; // unambiguously refers to Super.member
// value v3 = (sub of Super&Sub).member; // error: reference ambiguous between Super.member and Sub.member Note: I don’t propose this should be added (I think I’m still against it), but I think this fixes some holes of the original proposal. |
Is there any reason why you think this is better? I don't think it makes much sense for a type to inherit stuff from a type that it isn't subtype of... The I guess with my proposal, doing |
Not true, it can always be used to widen to a supertype (
|
Right. I got my thinking inverted. The I don't like the fact that an object can be an instance of two types that aren't related. What does |
abstract class C() of D {}
class D() extends C() {
shared String d = "d";
}
shared void run() {
C c = D();
// print(c.d); // error
print((c of D).d); // okay
}
Well that’s exactly the part that I also dislike :D how does your proposal deal with this? |
Sure. I was going to correct myself, but you beat me on that ;P
The type in the |
I generally get more confused when thinking about coverage than I do when thinking about subtyping, so sorry for the derps ;P |
Yeah, coverage is confusing. But I feel like adding the ability to freely add new supertypes to any existing type might break the soundness or decidability of the type system. It just feels way too powerful to me. That’s why I thought restricting it to coverage might be better. |
I don't think so... Specially because they are just interfaces. All you are saying is "add this interface to that type's I also am unsure if it's right to allow you to use those interfaces as types, to explicitly satisfy them, or to allow they to satisfy stuff... That could be possibly be problematic, but I haven't thought about this too much... |
But a type doesn’t have a With this proposal, in the code X0&X1&X2&X3&X4&X5&X6&X7&X8&X9 x = nothing;
value v = x.member;
|
This looks a lot like (but perhaps not exactly like?) a notion we discussed extensively several years ago under the title "introductions". You can see some of that discussion here. The idea was dropped because there were quite serious concerns about implementability on the JVM, and because it introduced some ambiguities that would impact modularity. Introductions are quite a lot like (but admittedly not quite as bad as) implicit type conversions. |
I have a slightly amended proposal: shared interface Super
abstracts Sub1 | Sub2 {
shared default String member => "";
} For this to be actually useful, What happens if the owner of this third module adds a member named |
As I said above, // module/package some.module
shared class Sub()
{
shared String member => "Hello";
} // module/package another.module
shared interface Super
abstracts Sub
{
shared String member => "Goodbye";
} // module/package my.module
import some.module
{
Sub
{
hello = member
}
}
import another.module
{
Super
{
goodbye = member
}
}
shared void run()
{
Sub s = Sub();
print(s.hello); // prints "Hello"
print(s.goodbye); // prints "Goodbye"
} |
So according to that, a seemingly innocuous change to |
I mean, basically all an introduction is is a single multiplexed object reference that could just as easily be represented using two distinct object refs. |
To be clear: the current rule is that addition of a member to a type cannot break clients of that type. It can potentially break a subtype, and thus, potentially, clients of the subtype. That's a risk you run when you use subtyping. Now, sure, we could say that you take on the same risk when you use introductions. And that might be OK if introductions added a whole lot of value. But I've come to the conclusion that they don't. And they would be quite difficult to implement unless you introduced some pretty draconian restrictions:
And probably some others I can't remember right now. |
Oh yeah now I remember another huge source of problems with this: if |
I would like to add that with this kind of additions to the language it's not sufficient to show that it can be done but it's necessary to come up with real examples that show how the new feature will make things better, how it will be an obvious improvement over the alternatives available to the language right now. We don't want Ceylon to be a kitchen sink of "neat ideas". |
@gavinking, you can check all those things. The important thing here is that the introductions are listed in the same module as the interface being introduced. That gets around a lot of ambiguities and conflicts (though not the method one you point out). There still are a variety of challenges, but I figured I should at least rule those ones out. |
@RossTate but introductions defined in the same module are not very useful. |
That's not true. Suppose you're writing a pretty-print module. You create a Now, there is another practical use case that this doesn't address. Namely if you're using a database module and the pretty-print module and you want to make the queries in the database module implement |
@RossTate well that sounds like a case that can be adequately handled using a type alias for an intersection type. |
|
Besides the fact that the "standard library" is another module... |
The problem is still there. If you guys decide to add some other member to a type in the language module, some clients could break... |
@Zambonifofex WDYM? |
Suppose my module adds an attribute to |
@Zambonifofex well, yeah. Now, with something simpler like extension methods that might not be such a big deal, since you could just say that the extension method hides the real method, and that might be good enough. But honestly I just feel like none of these things are solving any real problems. |
Well,there isn't any particular problem I'm trying to fix. I gave you an example of where this could be useful (adding a It isn't because there aren't problems that something solves, that that something is not worthwhile. I mean, object orientation didn't come to fix any particular problem. It was just something that was pretty cool, and people started adopting it. I know this is a very loose comparison, but I think that this is one of the things that you need to try out in order to see more concrete usefulness. I'm not trying to solve problems here, but rather, to open up possibilities... I also think that there aren't many downsides. You can only ever break stuff if you change the imports of your module. And in that case, the compiler will instantly warn you. It's a 3 minute fix. Go to the file, refactor, add an explicit import, repeat. It isn't something that should happen frequently either. A member needs to be added that have exactly the same name as the member in the abstracting interface in order for something to happen. I guess that it could harm decidability, but I can't think how. It doesn't feel like a potentially-dangerous feature to me... |
@Zambonifofex I can assure you OO was not created and adopted by people just because "it was cool". At that time you either programmed in C, Pascal, Basic (of not assembly) and for many people not ending up with this big plate of spaghetti code was a huge problem. So language designers were always experimenting with ways to modularize code. And in the end OO won out. It still remember it as this paradigm shift that made you think about objects and relations and whatnot. Nowadays we might discuss the actual merits of OO but back then they seemed pretty obvious. Now, Scala on the other hand, for many of us is the prime example of a language that is just chock full of things that are "cool", some turn out nice, others not so nice. But the creators admit it's because they see it as an academic language where you can try out new ideas. But we always talk about our "complexity budget", each thing you add to a language has to be weighed: the advantage it gives the programmer against the cost of learning it (naively one might say: if you don't understand it, don't use it, but you still have to read and understand other people's code, a big problem IMO with languages like C++ and Scala) So that's why Gavin says "I just feel like none of these things are solving any real problems". Unless you can show some real advantage (or low "cost") you'll meet resistance. And even when we had examples of "real advantage" we've sometimes agonized over adding things to the language, see for example Tuples and Constructors. But that's because we kept running into situations where code was obviously worse without them so in the end we decided it was (probably) worth the trouble. |
Well, you can add member to already existing classes, and allow classes that are aware of that override those methods. WIth a modified version of this concept, you could add methods only to classes with certain type parameters, and even make them satisfy new interfaces. This feels like a nice approach to simple conditional inheritance... You could, for instance, have predicates inherit a common supertype of booleans, and allow people to use I don't see that being too hard to understand. It straightforwardly adds a supertype to an existing type... I think it could be really useful - in ways we can't maybe see yet - for not much learning cost. Object orientation indeed solved a problem. But people didn't knew it would solve it yet... Developers started adopting it, because it looked like a cool thing, and only after it was being used, that people could more clearly see the advantages of it, and that's what made the concept grow popular. People found ways to solve things that weren't considered problems before. |
It's not about being hard to understand. Having too many features or several ways of doings things incurs a cost in itself. That's why we talk about "complexity budget", and that's why simpler languages are often more popular.
I think that for many developers that's never how it goes. They have work to do, want things finished yesterday, hopefully with as little work as possible. For them "cool things" are a luxury and often an obstacle. Scala's coolness is an obstacle, it makes code hard to understand if you're not proficient with the language (see http://www.scala-lang.org/old/node/8610). That's explicitly not where we want to go. I'm not saying this is not a useful feature, but it being "cool" is not going to help it get adopted, giving examples of the things that you can do with it and how the alternative without it would be much worse will. |
Agree with @RossTate, shared interface Formatable {
shared formal String format();
}
shared class SdtClassLib() { }
shared class AdapterStdClassLib(SdtClassLib wrapped)
satisfies Formatable {
shared actual String format() { return "X"; }
}
void fun() {
SdtClassLib x = SdtClassLib();
// here, to use Formatable interface, you have to know that
// the implementation of the classe of x is AdapterStdClassLib
// (and even more if there are other implementation for SdtClassLib's subclasses).
AdapterStdClassLib adap = AdapterStdClassLib(x);
adap.format();
} The pb here is that you have to know the AdapterStdClassLib class in function "fun" (and more if StdClassLib has subclasses with other adapter). With the proposition, you could have something like shared interface Formatable
abstract SdtClassLib {
shared actual String format() { return "X"; }
}
shared class SdtClassLibSub() extends SdtClassLib() { ... }
// Another implementation for the sub class
shared interface Formatable
abstract SdtClassLibSub {
shared actual String format() { return "sub X"; }
}
void fun2() {
SdtClassLib x = myFun();
// automatically take the good implementation of Formatable
// of the real class of x
x.format();
} here, i don't know if x is of class SdtClassLib or SdtClassLibSub, and then which method format will be actually call (exactly the same way if these two classes were satisfies Formatable interface directly). |
@gavinking, that's a cool solution! The one downside is that it may have poor performance due to having to case-match in the JVM. But that me a reasonable tradeoff. |
No, you do not. That's my point. You would in Java, but not in Ceylon, since Ceylon's type system is just so much more powerful. shared alias Formattable
=> String | Integer | Float | CustomFormattable;
shared interface CustomFormattable {
shared formal String format;
}
shared String format(Formattable arg)
=> switch (arg)
case (is String) arg
case (is Integer) formatInteger(arg)
case (is Float) arg.string
case (is CustomFormattable) arg.format;
shared void printf(String text, Formattable* args)
=> print(args
.map(format)
.fold(text)
((str, arg) => str.replaceFirst("$", arg))); There are no adaptors in sight! |
@Zambonifofex @RossTate Note that using the pattern above, one can achieve quite a great deal of what can be achieved using introductions. (Except of course you have a regular function invocations instead of postfix-style method invocations.) What it doesn't provide is the ability to make an object from a third library masquerade as a Of course, that problem can be solved using a wrapper: class Unformattable2Formattable(unformattable)
satisfies CustomFormattable {
shared Unformattable unformattable;
format => unformattable.string;
} Now, sure, that requires you to call printf("$ $ $ $", "hello", 1.0,
myCustomFormattable,
Unformattable2Formattable(unformattable)); But I don't see that as really that painful, frankly. |
I think we should be able to declare supertypes of existing types. Declaring superclasses would be fragile, but I think declaring superinterfaces could be pretty cool. Let's suppose I want the
Object
class to have one more member.html
, for example, which returns a DOM representation of the object. I could do the following:Whenever someone imports our module, and our
DOMRepresentable
interface, they see thatObject
contains ahtml
attribute, and them can override it in their subclasses.This interfaces have to provide an implementation for all their members.
This could be a neat way to add members to types that aren't represented by classes/interfaces.
I'm unsure whether there should be a special
super
annotation for those interfaces, nor if they should be implementable, or usable as types. I certainly don't want supertypes ofAnything
, specially it they are populated with objects that aren't instances ofAnything
...The text was updated successfully, but these errors were encountered: