Skip to content
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

Add basic interface capabilities. #1126

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

mhermier
Copy link
Contributor

@mhermier mhermier commented Dec 1, 2022

Hi,

The following change sets adds basic interface capabilities to the language.

For now, only the implements operator is added. But I also plan to add class definitions checks soon (I won't have the time to implement it, until the end of weekend).

If there is enough interest, I consider adding interfaces definitions to the language. I started prototyping it but It needs to be more polished.

@mhermier mhermier marked this pull request as draft December 1, 2022 09:11
@PureFox48
Copy link
Contributor

Well, I for one, am certainly interested in this proposal though I'm curious about what exactly you have in mind as I can't tell much from what you've posted so far.

At present, I simply use abstract classes for interfaces i.e. no constructor and methods are just shells to be implemented by the child classes. For example:

class Comparable {
    compare(other) {
        // This should be overridden in child classes to return -1, 0 or  1
        // depending on whether this < other, this == other or this > other.
    }

    < (other) { compare(other) <  0 }
    > (other) { compare(other) >  0 }    
    <=(other) { compare(other) <= 0 }
    >=(other) { compare(other) >= 0 }
    ==(other) { compare(other) == 0 }
    !=(other) { compare(other) != 0 }
}

class Date is Comparable {
    //...

    compare(other) {  
         // code to compare dates
    }
}

This works fine as long as I only want to implement a single interface and don't want to also inherit from a 'concrete' class, given that we don't have multiple inheritance.

So do you have something like this in mind:

interface Comparable {
    // code as above
}

class Date implements Comparable {
   // code as above
}

except that a class could implement multiple interfaces as well as inherit from a single concrete class?

If so, then that would definitely be useful though a problem to resolve would be how to deal with inherited and/or implemented methods with the same signature.

Another interesting possibility is that we might be able to use interfaces which provide default implementations of their own methods as a kind of 'mixin' which all implementers could share.

@PureFox48
Copy link
Contributor

Actually, thinking about it some more, I'm not sure that dealing with inherited and/or implemented methods with the same signature would be much of a problem for Wren.

If the child class included an implementation of such a method, then that could be considered as implementing the interface(s)'s methods as well as over-riding the inherited method.

If the child class didn't include an implementation of such a method (I imagine you won't want to force a child class to implement all interface methods given the limitations of a single pass compiler), then the inherited method would be used if there was one, otherwise the interface methods would be chosen in the order they appear in the class definition and might be empty in any case.

@mhermier
Copy link
Contributor Author

mhermier commented Dec 1, 2022

For now I think I would go simple for interface. That means I would only allow classs and interfaces to implements multiple interfaces. And an interface is only composed of "pure virtual" methods (without body).

I'm considering to add static methods to interfaces. Since there is a unique decoupling in wren about static methods being not inherited, it can bring some value to be able to provide a construct like function, and abstract the implementation details.

For now, I think interface is more like a concept or class shape then a real interface. If we want stronger interface, than is operator will need to be modified to also react on interfaces, and Class will need to remember the interfaces it implements.

What @PureFox48 suggest would make them act more like mixins/traits, while I see great potential about it, there are some technical limitations in the VM that needs to be modified to allow that, and more complex decisions to take about overload resolutions and how to access specific overloads. So I think, for now, I would delay the decision so that this patch-set lands first.

@PureFox48
Copy link
Contributor

OK, thanks for the clarification.

I agree, of course, that it's best to walk before we try and run :)

@PureFox48
Copy link
Contributor

Incidentally, won't you need to reserve interface as well as implements?

Even if interfaces are just going to provide pure virtual methods (and possibly static methods), there will need to be some way to distinguish them from ordinary classes.

@mhermier
Copy link
Contributor Author

mhermier commented Dec 1, 2022

Yes, if it takes that form. Unless the final decision is to go mixin and that keyword or another will have to be reserved instead. But currently there is no urge to restrict that yet.

extends definitions in class declaration can be restricted to consider classs as shapes/interfaces (like an implicit cast to an Interface or what ever its name). That way all methods must be user defined and so the facade (like in the pattern) would be well defined.

That way, we can have the basic functionality to toy with and can decide these more heavy decisions later, while (I guess) preserving backward compatibility.

@ruby0x1
Copy link
Member

ruby0x1 commented Dec 1, 2022

static methods being not inherited

This is likely changing, I've been testing it the last few months after discussing with Bob and it's been valuable in practice (and removes one surprise for users).

given the limitations of a single pass compiler

Side note @PureFox48 as you know we don't have type info, so the compiler side is basically pass through on this.


For Wren it seems the value in just confirming the shape of something matches expectations without relying on inheritance, akin to a structural subtype test for code. I know I rely on the shape of things implementing the shape of something, having it be able to be tested makes intent a lot clearer too in the code.

method(expect_shape_of_rect) {
   //typically compiler doesn't know the type of arg, or it's class, nor has details about the rect shape
   
   if(!(expect_shape_of_rect implements Rect)) Fiber.abort("expected a Rect-like value")
   
   expect_shape_of_rect.width ...
}

Optional type annotations raise the typical questions about union and multiple interfaces, e.g typescript has thing: Rect|Box|Queryable for overlapped types (like String|Null) to think on

@PureFox48
Copy link
Contributor

given the limitations of a single pass compiler

What I was originally thinking here is that if, say, you defined an implementing class before an interface in the same module, then the compiler wouldn't know what methods the class needed to implement. But I've just realized that you can't even do this just now for inheritance:

class B is A {}

class A {}

If you try to do that you get: Class 'B' cannot inherit from a non-class object. So there would be no reason to expect it to work for interfaces either.

This is likely changing

Interesting! Can't think of any obvious drawbacks to static methods being inherited (super could refer back to a static rather than an instance method of the parent class) so that would be a welcome change.

Optional type annotations raise the typical questions about union and multiple interfaces

Yeah, that would be a 'three pipe' problem for sure :)

@mhermier
Copy link
Contributor Author

mhermier commented Dec 4, 2022

Updated to support class definitions, add documentation.

@mhermier
Copy link
Contributor Author

mhermier commented Dec 4, 2022

There are obvious drawbacks to enabling static functions: constructor, constructor static methods, singleton pattern implementation and probably others. You don't want them to be inherited by default.

@PureFox48
Copy link
Contributor

Well, I’d assumed that constructors, which are a combination of a static method to create an instance and an instance method to initialize it, wouldn’t be inherited but I’d overlooked static methods which call constructors which Wren needs because of the lack of constructor chaining and also to implement the singleton pattern.

You’re right that we certainly don’t want those methods to be inherited by default so it looks like we’d have to introduce a ‘sealed’ keyword (or similar) to stop that from happening if static methods are to become inheritable.

Getting back to this proposal, I see that you are in fact forcing the subclass to implement the interface methods if ‘subclassResponsibility’ is used. Rather than using some special word like that how about just omitting the body entirely in the interface?

@mhermier
Copy link
Contributor Author

mhermier commented Dec 4, 2022

Getting back to this proposal, I see that you are in fact forcing the subclass to implement the interface methods if ‘subclassResponsibility’ is used. Rather than using some special word like that how about just omitting the body entirely in the interface?

This is next phase of the plan, currently this is the minimal requirement to make the idea work. And I want to wait for approval or comments for update, before moving on.

It will probably implemented as a 'mixin' with the extra requirement to not allow local variables (to save on complexity). That way, we can allow pure virtual and basic helper methods implementations.

@mhermier
Copy link
Contributor Author

mhermier commented Dec 9, 2022

I have struggles at implementing it. I don't know what to do. 'interface' and 'mixin' are both interesting features. problem being that Class and MixIn should have a toInterface method. While Interface can be imitated/implemented using a MixIn that only has virtual method, there is a small semantic difference that bugs me. So having an Interface would make it the third Type, probably as a parent class (or Interface to make things probably simpler)... But that makes a lot of new code in the global namespace, and I'm not very pleased that it happens.

@PureFox48
Copy link
Contributor

PureFox48 commented Dec 9, 2022

In that case I think I'd keep it simple and just do something like this for now:

interface Rect {
    width
    height
}

class Rectangle implements Rect {
   construct new(width, height) {
      _width  = width
      _height = height
   }
   width  { _width }
   height { _height }
}

That would define the 'shape' of a rectangle but leave the implementation of the methods to the implementing class. Judging by her remarks above, that seems to be all @ruby0x1 is wanting in any case.

It would be easy for folks to understand and, if a class implemented multiple interfaces containing a common method signature, the class method could be considered to satisfy all of them.

We can leave 'mixins' for another day.

@PureFox48
Copy link
Contributor

I'm not sure if this is exactly what you had in mind but, even with the simple scheme outlined in my previous post, classes could still have a toInterface method which would extract their instance method signatures into an Interface object, thereby saving you from having to create the latter explicitly.

The operation a implements b could then be defined so that b could be either an interface or a class. If it were a class, then the toInterface method would be applied under the hood.

I think you said or implied earlier that your idea (at least initially) was that a class would have a choice:

  1. It could inherit from another single class;

  2. It could implement one or more interfaces;

  3. It could inherit explicitly from Object.

This raises the technical question of how could a class choosing 2. also inherit from Object?

One solution would be for all Interface objects to inherit directly (or perhaps indirectly via an Interface class) from Object whose instance methods would then be inherited by an implementing class without being part of the interface itself.

@jube
Copy link

jube commented Dec 10, 2022

I don't see the point of interfaces in a language like Wren that have duck typing. There is no type checking in Wren at compile time and interfaces are a kind of compile-time type checking (does this class implements these methods?). At runtime, it is easy to see if a class does not implement a method : an error is generated.

On the contrary, mixins would add much value. What is in #1126 (comment) is a mixin, not an interface.

@PureFox48
Copy link
Contributor

Well the advantage of having interfaces in Wren would come through testing - instead of having to test for each method individually, you could test for the whole lot in one go via the implements operator.

Even if you don't test, there would still be the advantage of code failing earlier without waiting for an unimplemented method to be called.

Also, as @ruby01 said earlier, interfaces would make the intent of the code clearer too.

You're right, of course, that the example I posted earlier where I currently inherit from an abstract class would require a mixin rather than a simple interface to reproduce, given that I'm providing default implementations of the ordering operators.

I think we're all agreed that mixins would add plenty of value but it sounds, from what @mhermier is saying, that there are serious difficulties in implementing mixins and then treating interfaces as a special case. If those difficulties could be overcome, then that would represent a much more valuable solution overall.

@mhermier
Copy link
Contributor Author

The difficulties that I have is more logical than implementation wise. The problem is that when you add mixin, the notion of interface must exist in some form for testing. And this brings a lot of concept/code. This goes against the minimalism of wren.
About testing implements test shape of a class, but is could also be extended to test true interface inheritance. That way we could use some tag interface and other discriminating techniques...

@PureFox48
Copy link
Contributor

Well, I agree that we don't want to introduce both mixins and interfaces as separate entities into Wren - that would be too much extra baggage.

Given that we can already implement a mixin as an abstract class, I wonder whether a better approach would be to allow a class not only to inherit from another (single) class but to additionally implement one or more interfaces. This is what C# originally did (as a weaker form of multiple inheritance) and it seemed to work well enough.

The interfaces would take the simple form I outlined above.

This wouldn't be perfect as we would only be able to inherit from a single mixin though, in practice, you can often arrange matters so that the mixins inherit from each other.

However, name resolution shouldn't be a problem. If there were a signature clash, the interface(s) would be satisfied by either a method declared in the implementing class itself or inherited by it.

The problem I raised earlier about how a class, implementing one or more interfaces, could then inherit from Object would be solved automatically by this approach without the interface(s) having to themselves inherit from Object.

In the class definition, after is, the parent class (if there was one other than Object) would always come first followed by the interface(s) separated by commas. We could dispense with implements and just extend is for use with either classes or interfaces. We'd then need only one new keyword: interface.

Personally, I'd like to see an isnot operator introduced as well as i find the syntax for negating is quite tedious but this has nothing to do with this proposal as such.

Incidentally, in C# 8.0, they're now allowing interface methods to have default implementations so the above approach wouldn't necessarily rule out having multiple mixins in a future version of Wren.

@eritain
Copy link

eritain commented Dec 21, 2022

In Raku (and the ports of its object system to Perl and Javascript, Moose and Joose), interfaces are fully subsumed by "roles," which is their implementation of traits, intended to serve as the behavioral dimension of the type system. (That is, class Square "is" Rhombus and Rectangle, but it "does" Drawable, Translatable, etc.)

The most interface-like roles solely declare method names and signatures. It's a compile-time error for a class definition to declare that it does a role, but then end without implementing those methods.

(This only slightly impedes duck typing. If a class has all the right methods but does not declare that it does the role, and for whatever reason you're not free to add that definition, you can get the compile-time safety by declaring a class to inherit from it and do the role, or you can use the but operator to mix it into individual objects at runtime. Under the hood, runtime mixins actually just declare that class anyway and make a copy of the mixed-into object to instantiate it.)

Less interface-like roles can also declare attributes and define methods. When a class declares that it does such a role, the declarations are copied into the class and compiled (the class "consumes" the role). Crucially, it is a compile-time error for a class to consume two roles that give different definitions for the same method name and signature, unless the class resolves the conflict by providing its own definition.

(Definitions directly in the class supersede definitions composed in from roles, which supersede definitions inherited from superclasses. This generally turns out to be what you want. The superseded definitions are still available, but under qualified names. I believe something similar happens when attribute declarations have conflicting types.)

These compile-time errors manage to avoid a lot of the pitfalls of multiple inheritance, while still adding code reuse to all the benefits of interface types. A role definition can even include further does-role declarations, and there's no diamond inheritance problem or thinking through a method resolution order; if consuming roles gets you the same definition twice, no problem, and if it gets you conflicting definitions, the compiler makes you be explicit about what to do.

@PureFox48
Copy link
Contributor

Interesting.

I like the way that Raku resolves conflicts between role methods having the same signatures but different definitions by insisting that the consuming class provides its own definition. It's a possible approach for Wren if we just do bare interfaces for now, for which conflicts are not a problem, but add mixins later.

@mhermier
Copy link
Contributor Author

I'm currently really busy with the end of the year season. To make things worse, I'm currently hill, so my work on this is delayed for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants