This has come to mind as a result of the questions posed by #16725 and #16727, but we have toyed with it in passing before.
Checking a subset of languages that allow operator overloading, it looks like:
R K::operator +(S b); within the type definition of K) and stand-alone functions (R operator +(K a, S b);.__add__(self, other))Base.:+(x::SomeType, y::SomeType) = ...)Pros:
Cons:
Thanks for the language survey, which was going to be the first thing I asked about on this issue. I think it'd be good to know what Swift does here as well.
I think of the main challenge here as being how to handle the proc +(x: int, y: myNewType) case where we probably want to associate this + overload with myNewType, but as a method, it would most naturally be defined on int (your con 1). I think this is the case where Python supports a "reversed" version of each operator? Knowing how other method-based operator approaches deal with this would be interesting (and knowing how well-received vs. not the Python approach is might also be nice).
Maybe I'm forgetting the full discussion in #11717, but I thought that the fact that forwarding applies to methods was one of the benefits of this approach.
It looks like Swift allows operator overloading via methods (see the Operator Methods section).
I'll look a little bit more into Python. From the glances I did to make the language survey, I do recall Rust specifically caring about the direction the traits were implemented - their example showed writing both directions for adding a Foo and a Bar together, though I'm not sure what would happen if you didn't.
Maybe I'm forgetting the full discussion in #11717, but I thought that the fact that forwarding applies to methods was one of the benefits of this approach.
It could be I misread it as well - I thought the discussion seemed to trend away from the suggestion
Python has versions of the operators defined with __r (for "right"), which only fires if the left operand either doesn't have a __add__ method or its __add__ method doesn't work with the right operand's type. So you aren't guaranteed to have __radd__ called if you define it.
It looks like Rust requires you to write both directions for the Add trait if you want to be able to reverse the arguments. And C++ similarly seems to need an implementation with the type on the right side to work on that case as well?
My quick read of the Swift entry makes it look less like a method and more like a standalone function that's associated with a type (similar to Michael's third proposal on #16725). At least, it appears to be operating on two arguments rather than this/self and a single argument.
I think you're partially right - it's a type method, rather than a method on an instance or involving self/this. It's defined within an extension of the class in that specific example:
extension Vector2D { // Note this declaration, tying it directly to the type
static func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
I think you're partially right
Which part do you think I got wrong?
I didn't grok on first read that "type method" in Swift apparently means the same thing as it does in Chapel. But I'm not sure why it's considered a type method, considering that you don't write Vector2D.+(myvec1, myvec2) or Vector2D.(myvec1 + myvec2). That's what makes me think that they're leveraging the type method syntax to associate the operator with the type as in Michael's third approach, but without otherwise treating it like a type method. Or, to put it another way, operators seem like a pretty special case in Swift (at least, I haven't found anything else saying that type methods behave this way for other cases).
Full disclosure, I do not know very much about Swift syntax. But my read of that section was that putting code inside extensions like that was their main way of associating later methods with a type, more similar to our secondary methods. Basically, I was interpreting
The operator method is defined as a type method on Vector2D, with a method name that matches the operator to be overloaded (+). Because addition isn鈥檛 part of the essential behavior for a vector, the type method is defined in an extension of Vector2D rather than in the main structure declaration of Vector2D.
as implying that you can't define methods outside the body of the type without using an extension wrapper. I think syntactically the block looks like what Michael is proposing, but it's leaning on the normal way of extending a class rather than being an additional syntax like what it would be for us if we added that particular syntax. It also (again, based on a very limited understanding) seems like using these extension blocks is like the body of the type is "reopened" - e.g. it would be like if instead of writing:
record Foo {
var x: int;
}
proc Foo.bar() { ... }
we instead had to write
record Foo {
var x: int;
}
extension Foo {
proc bar() { ... } // You don't put `Foo.` prior to the name because we're in an extension block
}
Backing that interpretation up, I looked at the Methods section of the Extensions definition and it doesn't seem to include type prefixes when defining additional methods
putting code inside extensions like that was their main way of associating later methods with a type, more similar to our secondary methods.
I agree with that. So I think what you're saying is that it's the special-ness of defining operators as type methods that makes them associated with a type rather than the extension block itself. I agree with that (but note that I didn't say _which_ construct I thought made it similar to Michael's third proposal :) ).
Maybe put another way, I think the Chapel translation of Swift's approach to operator overloading would be to write:
record R {
// as a primary method
proc type +(lhs: R, rhs: R) {
return new R(lhs.x + rhs.x);
}
}
or:
// as a secondary method
proc type R.+(lhs: R, rhs: R) {
return new R(lhs.x + rhs.x);
}
On the plus side, this is short and sweet and clearly associates the operator with a given type.
On the minus side, it doesn't really feel like a type method in any other way that I can detect (again, since it's not called on the type but on two instances of the type). I wonder whether it's worth trying to engage someone on the Swift team to see whether they can rationalize / explain the choice better. If there were a clear argument for why it made sense that I'm missing, I'd be open to it (and I might even be open to it without a clear argument, but I wish I could rationalize it better to myself).
I asked in the Swift forums about their approach to defining operators this morning and got one fairly quick response (so far):
I read the document linked from that discussion - https://github.com/apple/swift-evolution/blob/main/proposals/0091-improving-operators-in-protocols.md . I come away with a positive impression of this design & I appreciate that write-up.
Brad asked in the swift forum:
If you (or others reading this) know: Are there other type methods in Swift that can be invoked without applying them to a type name, or are operators unique in this regard? (I'm guessing this pattern is unique to operators?).
Unlike in C++ (say), you cannot call a type method on an instance. I.e.
class TestClass {
var x = 0;
static func test() {
print("hey");
}
}
var c: TestClass = TestClass();
TestClass.test(); // OK
c.test(); // does not compile
This makes me think that it's unique to operators. The SE-0091 document also arguably says so:
Instead, Swift should always perform operator lookup universally such that it sees all operators defined at either module scope or within a type/extension of a type. ...
While it may seem odd that operators will be the only place where Swift does such universal lookup, operators can be considered a special case. ...
For a minute I thought Swift had universal methods the way that D does (so you can write myClass.myMethod()alternatively as myMethod(myClass)) but it does not do that. One thing I found curious is that you can write MyClassType.myMethod(myClass)() as an alternative way to call it (it seems that MyClassType.myMethod(myClass) returns a closure containing the called function with the this argument being myClass). (The reason I care about any of that has to do with constrained generics situations where you want to choose the interface you are working with; IIRC we were thinking of supporting something like (genericArgument:SomeInterfaceType).interfaceMethod() but SomeInterfaceType.interfaceMethod(genericArgument) or SomeInterfaceType.interfaceMethod(genericArgument)() might be reasonable alternatives. I suppose that operators being type methods makes this a bit simpler - SomeInterfaceType.+(genericArgument, genericArgument). )
I also think this is a reasonable direction. Sounds like the three of us are at least in agreement :) I think it's worth running by the group as a whole before fully committing to it, but this seems quite promising to me (and view bringing it up to others as in my court)
Note that in Swift, operators are not required to to be static/type methods. E.g.
struct MyStructType {
var x = 0;
}
func +(a: MyStructType, b: MyStructType) -> MyStructType {
return MyStructType(x:a.x + b.x);
}
var a: MyStructType = MyStructType(x:1);
var b: MyStructType = MyStructType(x:2);
var c: MyStructType = a + b;
print(a)
print(b)
print(c)
However they are required to be static/type methods in protocols. I am not sure if a protocol would find a func + like above. Furthermore, apparently the non-method version can be called from a protocol using the static/type method. Adding to the above example:
public protocol MyAddable {
static func +(lhs: Self, rhs: Self) -> Self;
}
func double<T: MyAddable> (x: T) -> T {
return x + x;
}
extension MyStructType: MyAddable {
}
var d: MyStructType = double(x: a);
print(d);
This relates to a discussion Brad and I were having on https://github.com/chapel-lang/chapel/issues/16732#issuecomment-736645500 -
import GlueModule.{+, =, :} to import these standalone functions from a module defining neither type.So, it seems manageable either way in that regard.
I don't know why Swift chose to keep the non-static-method operators. It might have been for backwards compatability. AFAIK not because of a lack of something like tertiary methods because e.g. we can use a protocol and extension to add methods to a built in type like Int.
Note that in Swift, operators are not required to to be static/type methods.
Thanks for pointing that out, I didn't catch it. Since reading that sentence the other day and stewing on it, I've liked the idea of supporting operators as either type methods or standalone functions in Chapel. To me, this has the following advantages:
for operators that are strongly affiliated with a given type, it permits them to be supported on that type as methods in a way that (a) doesn't require a significant change to the language or compiler (apart from knowing how to find them during resolution), (b) doesn't introduce the asymmetry of a pure method, as in Python, with its associated reverse-operator issues, and (c) works well with use/import statements that bring types and their associated methods into scope.
for operators that aren't associated with a type (say ones defined on a pair of third-party types that don't know about each other), it doesn't require the operator to be associated with one of the two types arbitrarily (or worse, both of them).
it provides backward compatibility and, as a result, lets us postpone worrying about defining operators on ints as type methods (though the fact that the following code works suggests that it probably wouldn't be a stretch once we supported them on other types):
proc type int.answerDelta(delta: int) {
return 42;
}
writeln(int.answerDelta(0));
I should also clarify that in favoring method-oriented approaches in these discussions, I haven't necessarily been opposed to continuing to support them as standalone functions鈥攅specially knowing that Swift supports both鈥攏or to naming them in import/use filtering clauses. I just didn't want to have to do that for every operator when it had an obvious type to be associated with (nor did any of us), and was reluctant to associate general standalone functions with types or to make operators distinct from standalone functions in that way.
My initial reaction was that operators should be methods on types (I mean proc R.+ and not proc type R.+). However, reading the discussion, I am more convinced that type methods (proc type R.+) has merits. I have few concerns against the generally agreed-upon approach, but I am not necessarily objecting to it.
Because addition isn鈥檛 part of the essential behavior for a vector, the type method is defined in an extension of Vector2D rather than in the main structure declaration of Vector2D.
I interpreted this as the addition operator being sort of a factory function that generates a new value and as such it should be a type method. This interpretation also is in line with operators being standalone functions, I guess. Though I am less convinced about the generality of this argument. Incrementation += is more of a part of an instance as it is likely to modify it. Or = for that matter.
should also clarify that in favoring method-oriented approaches in these discussions, I haven't necessarily been opposed to continuing to support them as standalone functions鈥攅specially knowing that Swift supports both鈥攏or to naming them in import/use filtering clauses.
To me, having both feels a bit more than just having two things doing the same thing. The scope resolution difference between the two is important but probably subtle. And I am not sure if there are use cases where a user would want the standalone function's behavior, even though it is what we have today. After having type method-based operators, it'll feel like a not-so-well-working way of doing operator overloading.
I think there are very valid arguments against removing/deprecating them as it would be a big breaking change and may require some effort. Those may preclude us from doing it in the near term, but I'd like to think more like "we have old way of doing this and new and better way of doing this" rather than "there are two ways to do this".
Thanks for weighing in, Engin!
Though I am less convinced about the generality of this argument.
Incrementation+=is more of a part of an instance as it is likely to
modify it.
I don't disagree with this, but note that several other operators are also
not factory-like (at least not in terms of the type on which they're defined).
For example, < returns (or should return) a bool regardless of what
types are passed to it:
proc type R.<(lhs: R, rhs: R) {
}
such that < isn't an R factory function, but rather a bool factory function,
if anything. Yet it should still obviously be associated with R.
So, while it's tempting in some cases like + (at least, for homogeneous arguments that
return that same type), I wouldn't generally think of the rationale for the type method
approach to be "it's a factory" so much as "it's merely a 'trick' for associating an
operator with a type."
The scope resolution difference between the two is important but probably subtle.
I agree that it will require melding two different ways of finding operators together,
but I don't know whether it will be difficult or subtle (where I'm truly trying to say
"I don't know" not "I respectfully disagree" in a roundabout way... however, I could
imagine that it might not be so bad.
And I am not sure if there are use cases where a user would want the standalone function's behavior, even though it is what we have today.
I want to emphasize that I'm not suggesting we support them simply for the purposes of
backwards compatibility. While I expect that many (most!) operators will and should be
defined as type methods, I think an important case is a mixed-type operator where the
operator isn't logically attached to either of the two types. For example:
real128 type and Lydia defines a fixed point typeFor such a case, being able to define operators as standalone functions permits me to make a
Real128FixedOps module that defines the operators I care about without having to arbitrarily
associate them with one type or the other. That seems like a strength to me.
I could definitely imagine a style-checker compiler flag that would whine at you if you defined an
operator as a standalone function if its arguments were of the same type (or possibly even a
single user type and a primitive type), telling you that you should really make it a type method to
help shepherd people toward the type method approach.
@bradcray -- your response pulled me closer to having standalone operators in the language, but pushed me away from type methods :)
such that < isn't an R factory function, but rather a bool factory function,
if anything. Yet it should still obviously be associated with R.
So, while it's tempting in some cases like + (at least, for homogeneous arguments that
return that same type), I wouldn't generally think of the rationale for the type method
approach to be "it's a factory" so much as "it's merely a 'trick' for associating an
operator with a type."
Good point about the comparison operators. So, then, if a user asked why operators are type methods, would our response be "because we didn't know how else to mark them"? :)
Reading https://github.com/apple/swift-evolution/blob/main/proposals/0091-improving-operators-in-protocols.md, I don't know whether it was an option for swift to introduce a new keyword at that point. It still sounds like (I don't know swift), even though they are static functions on interfaces, there is some special machinery going on with respect to their resolution.
For such a case, being able to define operators as standalone functions permits me to make a
Real128FixedOps module that defines the operators I care about without having to arbitrarily
associate them with one type or the other. That seems like a strength to me.
I agree with this.
It still feels a bit awkward that this kind of idioms would result in things like import MyModule.+. Although + is just a function with a special call syntax, I feel that they are more part of the language as symbols. That being said, I think it is consistent, relatively easy to explain, and I don't have any alternative idea. As you said, it would be nice to have most of operators as non-standalone functions and reserve these to limited cases either through some documentation and/or compiler warnings etc.
So, then, if a user asked why operators are type methods, would our response be "because we didn't know how else to mark them"? :)
Something nearly that arbitrary, yeah. We wanted to associate them with types, and methods are a natural way to do that; but we didn't want to inherit Python's problems with true method-based operators (asymmetry between the two operators; reverse-operator style challenges with making int op this.type combinations); so type methods were a way to do this, where Swift provided a precedent.
I don't know whether it was an option for swift to introduce a new keyword at that point.
Yeah, interesting question. On email, Engin proposed a new operator keyword that might be interesting as a variant of the "type method" approach. E.g., rather than saying:
record R {
// primary operator
proc type +(lhs: R, rhs: R) { ... }
}
// secondary operator
proc type R.-(lhs: R, rhs: R) { ... }
// standalone operator
proc *(lhs: R, rhs: S) { ... }
what if we were to say:
record R {
// primary operator
operator +(lhs: R, rhs: R) { ... }
}
// secondary operator
operator R.-(lhs: R, rhs: R) { ... }
// standalone operator
operator *(lhs: R, rhs: S) { ... }
That is, introduce a new keyword to introduce operators where its use within the context of a type would result in behavior similar to the proposed "type method" approach and its use in a standalone mode would be similar to the standalone proc-based operators we have today?
It still feels a bit awkward that this kind of idioms would result in things like
import MyModule.+.
For my Real128FixedOps, I think this is a case where I'd just do use Real128FixedOps;, assuming that all it defined were operators. But for someone with a strict import mentality, I think that we should support the ability to do import MyModule.{+,-,*,/}; for orthogonality with other standalone functions.
Even if we took the operator keyword approach, I think I'd want to be able to write that import statement (i.e., be able to name standalone operators like procedures). With the keyword, we could also imagine doing something like import MyModule.operator; to say "bring in all the operators in MyModule", but this seems a little bit contrary to the typical import philosophy which is "I want to explicitly name the things that I want to bring in to avoid surprises", so I'm not a big fan of it (it's also pretty different from any other import/use filters we currently have).
Would operator as a sibling to proc only ever apply to functions with punctuation names? Either way (with operator or with the Swift-like approach) aren't we saying that there will be special handling for functions with punctuation names - namely they are visible if the type is?
Somehow I am more comfortable with this for operators-are-type-methods vs the operator keyword idea. Maybe it is because with the type-methods idea we could change our mind about this without having to revisit the strategy. (If we wanted to, we could make type R.foo() count as "associated" with R - i.e. it is visible when called on an R (or from myR.type) even if it would not be visible as a free function. Maybe this already works today? We could consider generalizing from there to make proc R.myAdd(lhs, rhs) be callable as myAdd(lhs, rhs).).
Is it inherent in an operator that it can't be an iterator? E.g. I could imagine defining + on two iterators to be another iterator that yields sums.
Would operator as a sibling to proc only ever apply to functions with punctuation names?
I was imagining "yes".
aren't we saying that there will be special handling for functions with punctuation names
I agree there is special handling in either case, if for no other reason than that with either approach, operators enjoy infix syntax.
namely they are visible if the type is?
If the operators are methods, yes. If they're standalone operators, I wouldn't consider them to be linked to any type.
To me, the attractive quality of the operator approach is that (1) it emphasizes that these things are different beyond just their symbolic names (again, where the infix notation is an obvious way); and (2) they don't try as hard to masquerade as something they're not (i.e., "if this is a type method, why do I get to write x + y rather than needing to write R.+(x, y) or R.(x + y)?) Whereas introducing a new declaration style in operator permits the operation to be associated with a type, given a symbolic name, and called in an infix manner without any changes or abuses of how type methods work.
If we wanted to, we could make type R.foo() count as "associated" with R
Just to make sure I'm understanding this, are you saying that if I were to write:
record R {
}
proc type R.foo() { writeln("In Brad's foo!"); }
then maybe I could be permitted to simply say foo() if R was in scope rather than R.scope in order to remove the non-orthogonality between R.foo() and not having to write R.(x + y)?
If so, I'm not particularly excited by that idea because it seems like it just increases the abuse of what is traditionally meant by a type (or static) method. It also feels dangerous to me that, in an import-based world, one could say import M.R; and effectively have standalone functions injected into their scope that didn't necessarily have any relationship to R, like my foo() above (beyond how it was declared). That is, if the goal of import is to make namespaces very precise, and I can get things like foo() injected into my namespace without naming them in the import or accessing them through R or myR, that feels like it opens up a potential for abuse / hijacking / surprises for import-based users. (Yes, operators would enjoy a similar kind of exception, but... I don't really see any way around that apart from requiring the R.(x + y) syntax to use them.
Is it inherent in an operator that it can't be an iterator? E.g. I could imagine defining + on two iterators to be another iterator that yields sums.
Interesting question (and somewhat parallel to asking "Can I write iter +(...) today?" where I was going to assert that the answer is "no", but it actually seems to be "yes"). I'd be inclined to say that operator + should not support the ability to write yield statements within its body similar to how proc doesn't today and use some other keyword (operiter :) ) if we wanted to support that case.
then maybe I could be permitted to simply say foo() if R was in scope rather than R.scope in order to remove the non-orthogonality between R.foo() and not having to write R.(x + y)?
I was first saying you could write f().foo() where f() returns type R even if R isn't visible at the call site. I think we can do this today. Then I was saying, we could consider extending it to handle behavior like the operators. I wouldn't expect that to make sense with no arguments though since there is no type to associate with. It'd be more like if we have operator R.bar(a: R, b: R) then we can find it if you call bar(myR, myR).
I think a big part of my reaction is that I'm not convinced that the available visibility rules should be connected to whether or not something has a punctuation name. Hence I like directions where we could extend the method-like visibility rules to operator-like things even if they have non-punctuation names (like bar in the example above). I like these directions even if we choose not to allow the feature to be used with non-punctuation names right now.
For a concrete example - say we decide to add support for user-defined casts and implicit conversions (issues #16732 and #16729). We have in mind an operator that could be used for casts (:) but the implicit conversions will probably be supported by something less punctuation-y (e.g. canImplicitlyConvertTo). It seems to me that we'll want the method-like visibility rules for these functions in most cases. If we decided to make the cast function spelled out as cast instead of being an overload of : would that mean we can no longer have method-like visibility rules? Would that mean we would have to introduce some new punctuation to mean "can implicitly convert to" so that the implicit convertibility can use method-like visibility rules?
I'd be inclined to say that operator + should not support the ability to write yield statements within its body similar to how proc doesn't today and use some other keyword (operiter :) ) if we wanted to support that case.
Eh, then why isn't operator a modifier on proc/iter? Like operator proc + or operator iter + ? (Then it is arguably a different syntax for a "type associated proc" from https://github.com/chapel-lang/chapel/issues/16725#issuecomment-729168101 and could generalize to non-punctuation functions, if we wanted it to - where generalizing it would mean redefining "operator" from "a function with a punction name" to "a function with type-associated visibility rules").
I do find operator vs operiter pretty funny, FWIW 馃槂
It'd be more like if we have operator R.bar(a: R, b: R) then we can find it if you call bar(myR, myR).
...
Hence I like directions where we could extend the method-like visibility rules to operator-like things even if they have non-punctuation names
I still don't like that on similar grounds to the above: In a use case where I've tried to be very precise with my imports, I'm getting an identifier injected into my scope that I didn't request. Wasn't the point of introducing import to prevent such cases? Why should your bar get to sneak into my scope?
For casts, it seems as though defining them in terms of the symbol used to express the cast makes the most sense. For coercions, I'd be inclined to either (a) rely on some other special method in the family of init, init=, or deinit, (b) introduce an operator that means coerce but doesn't show up in user code, or (c) permit operator to be applied to something specific and non-punctuation-y (while not permitting it to be applied to foo and bar).
Eh, then why isn't operator a modifier on proc/iter? Like operator proc + or operator iter + ?
It certainly could be. I'm mostly still reeling at the realization that we support iterator operators today and trying to decide whether that's a good or bad thing. From what I can tell, there isn't any code in the repository that makes use of it.
permit operator to be applied to something specific and non-punctuation-y (while not permitting it to be applied to foo and bar).
I don't think we necessarily have to limit to punctuations. But more like you cannot just define operator foo() willie-nillie and operators must have names defined in the language. For example, it wouldn't bother me to have a operator cast that is used for :, in itself. It would only bother me because we'd have operator + and not operator add. (Today I'd definitely argue that we should have operator :)
Eh, then why isn't operator a modifier on proc/iter? Like operator proc + or operator iter + ? (Then it is arguably a different syntax for a "type associated proc" from #16725 (comment) and could generalize to non-punctuation functions, if we wanted it to - where generalizing it would mean redefining "operator" from "a function with a punction name" to "a function with type-associated visibility rules").
operator proc is a viable alternative to me, though it would make more sense if we were to have operator iter, too. And I cannot make sense out of operator iter :)
For a concrete example - say we decide to add support for user-defined casts and implicit conversions (issues #16732 and #16729). We have in mind an operator that could be used for casts (:) but the implicit conversions will probably be supported by something less punctuation-y (e.g. canImplicitlyConvertTo). It seems to me that we'll want the method-like visibility rules for these functions in most cases.
I am not sure if I follow, but a function that specify whether a type can coerce is related to operators only vaguely because it is similar to casts. So, it would make sense to me to have operator R.+ in addition to proc type canImplicitlyConvertTo, where the latter is pretty much analogous to things like dsiSupportsPrivatization to me.
Most helpful comment
I agree with that. So I think what you're saying is that it's the special-ness of defining operators as type methods that makes them associated with a type rather than the
extensionblock itself. I agree with that (but note that I didn't say _which_ construct I thought made it similar to Michael's third proposal :) ).Maybe put another way, I think the Chapel translation of Swift's approach to operator overloading would be to write:
or:
On the plus side, this is short and sweet and clearly associates the operator with a given type.
On the minus side, it doesn't really feel like a type method in any other way that I can detect (again, since it's not called on the type but on two instances of the type). I wonder whether it's worth trying to engage someone on the Swift team to see whether they can rationalize / explain the choice better. If there were a clear argument for why it made sense that I'm missing, I'd be open to it (and I might even be open to it without a clear argument, but I wish I could rationalize it better to myself).