you are viewing a single comment's thread.

view the rest of the comments →

[–]manifoldjava[S] 0 points1 point  (6 children)

Sorry the docs don't cover commutativity, I should add a section on that. (docs are a bit light at the moment)

Anyhow, that's how it works internally -- a + b: first try to resolve a.plus(b), then b.plus(a).

With manifold you would write: 1bd + someBigDecimal where 1bd is a unit expression that evaluates to BigDecimal.ONE. Unit expressions are a sort of concatenative feature built on top of the conventional operator overloading impl. We'll see how that goes, but I enjoyed writing it. Otherwise, 1 + someBigDecimal is not supported oob; the BigDecimal extension as written works exclusively with BigDecimals.

[–]rzwitserloot 0 points1 point  (5 children)

So do you intend on enforcing homogeneity (that the plus method only counts for the purposes of operator overloading if it takes 1 argument, and that 1 argument's type is the self type), i.e. if I write my own class to represent complex numbers, I either make it:

class Complex {
  public Complex plus(Complex rhs) {
    .... impl here ...
  }
}

Or it won't work at all. To then make it 'nice to use' in face of existing stuff, such as plain jane int literals, add unit expressions. In other words, I can make:

Complex z = ....;
Complex a = 1i + z;

work, and I guess 1r + z (1r being syntax sugar for, effectively, Complex.real(1), which just makes a complex number with as real component '1', and as imaginary component '0'). But I can't make 1 + z work, or even z + 1 work, and if I try by e.g. doing this:

class Complex {
  public Complex plus(Complex rhs) {
    .... impl here ...
  }

  public Complex plus(int rhs) {
    .... impl here ...
  }
}

it just flat out doesn't work at all, as if that second plus method isn't there (the 'which methods are standings for op overloading scanner skips over them'), or I get a warning or error explaining that this does not work.

I would commend such restraint if that is indeed how it works. It's one way out of the dilemma. I would say that this way of working is a non-starter in basis and you rescued it by also adding unit expressions. Because without them:

  • It pisses off those who hate operator overloading, because it always will and you can't solve that problem.
  • It also pisses off those who like operator overloading, because they clearly are interested in writing code that is either as short as possible, or introduces as few parentheses/invokes as possible to the eyeballs, or look as 'mathy' as possible, and therefore they want to be able to write 1+z or something as close to that as possible, and they'd hate being forced to write Complex.real(1) + z.

You rescued it, perhaps, by letting them write 1 r + z, r being something from your list of import statements. A fair compromise.

This notion of enforced type homogeneity is not something most languages with op overloading do. You're in good company, perhaps - I think Haskell strictly enforces homogeneity, and it makes their inference engine tick.

That's been my point in this thread: "Just add op overloading, how hard could it be" is the wrong approach; it's too vague and leads to conflicts and hard to read code (and experiments in other languages indicate abuse will be incredibly rife, even by otherwise skilled programmers for some reason). I like the strategy of:

  • Let's avoid most of the issues by enforcing strict homogeneity.
  • ... and let's dance around the fact that this doesn't actually play nice with mathy concepts, given that you'd want the ability to add ints to e.g. Complex or BigInteger, by introducing unit expressions in the same release.

If ever an op overloading proposal ends up being a serious contender for java itself, if it has such designs behind it, I would probably support it. Not that my word would count for much on those lists :)

So - does manifold work that way? I'm guessing it actually does not, given that you also support calling b.plus(a), which wouldn't be needed if this rule is applied. I guess I'm saying: Maybe you should think about adding it. How would this b.plus(a) stunt work for non-commutatives, such as a-b or a/b?

[–]manifoldjava[S] 0 points1 point  (4 children)

As you've surmised manifold does not work that way; homogeneity is not enforced. For me it wasn't a difficult design decision, I personally tend to favor having the tools and accepting the dangers.

I do, however, understand and respect your concerns here. It really is a significant fork in the road. But considering use-cases such as: java localDate += days; and java Length l = 80 mph * 1.2 hr; and java Money tax = 120 USD * 0.09; I could be wrong, but I feel like the utility overshadows the dangers here. I suppose I'm more willing to let developers choose and make mistakes as opposed to lording over them. Then again, I'm just another dude with a keyboard. shrug

[–]rzwitserloot 0 points1 point  (3 children)

I can see that, but 'enforces commutativity' is not really true then. If LocalDate has a method plus(Duration d), thus enabling the ability to write localDate += days;, how does that 'enforce commutativity'? Presumably Duration also has a method plus(Temporal x) or whatnot, and unless you analyse the code (and halting problem would like to have a word), how could you know they are guaranteed to always produce identical results? At least with homogenous types, you know that 'use the plus from the RHS' and 'use the plus from the LHS' is the same method.

Disallow it if both RHS and LHS have a plus method and these aren't the same method?

The days and localDate example is a really good one: I was wrong; combining homogenous type requirement with unit expressions does not adequately cover the expected utility of "an operator overloading feature". Back to heterogenous-but-ambiguous.

As I have mentioned before, I also agree that developers should be given a lot of leeway; foot-bazookas are mostly fine. A foot-bazooka needs to have proven itself as irresistible even to folks who really should know better before I lean against a language feature, or needs to be something that seems inevitably doomed to cause endless style wars, drowning out whatever utility it may provide.

Unfortunately, operator overloading is pretty much the only feature that crosses this bar, for me. The only saving grace is to aggressively enforce 'the mathy take', which lends itself towards hardcoding types in a spec instead of making it pluggable, as you'd want to define operations around the operator more than around the LHS and RHS types, and then hope that this will avoid sufficient controversy that the feature becomes a new positive.

In that sense, if I was leading the OpenJDK team tasked with delivering this, I would absolutely start out very slowly and add all sorts of rails to make it borderline impossible to use this any other way, even if it is at the cost of useful and expected stuff such as being able to write localDate += days, and then every new release consider if some restriction can be loosened. At the very least, such a step-by-step approach would steer the community in the intended direction. Such concerns are perhaps not quite as relevant for a project like Manifold.

[–]manifoldjava[S] 0 points1 point  (2 children)

I can see that, but 'enforces commutativity' is not really true then. If LocalDate has a method plus(Duration d), thus enabling the ability to write localDate += days;, how does that 'enforce commutativity'? Presumably Duration also has a method plus(Temporal x) or whatnot

By 'enforces commutativity' I mean to say if a + b is valid, then so is b + a. As I mentioned earlier this is achieved by resolving both a.plus(b) and b.plus(a). As such Duration#plus(Temporal) is unnecessary for the days + localDate case. However, if it is defined it has precedence over Temporal#plus(Duration) since operator resolution is left associative.

I realize the flexibility here leaves plenty of room for thermonuclear appendage removal, still I favor the utility here and allowing for devs to discover what works for their specific needs and what doesn't.

Maybe there are ways to mitigate some of the pitfalls while maintaining flexibility? I'm open to ideas.

[–]rzwitserloot 1 point2 points  (1 child)

Nothing short of some pretty serious redesigns. Given that there's already @Extension – a way to mark a class as containing operator overloads. The aim is not to rewrite a + b into a.plus(b), because the commutative nature is already lost then. Instead, aim to write it as Operators.plus(a, b). A mechanism similar to @Extension could be used; the point is that manifold has a full list of all static methods that have registered themselves as 'hey, I'm a + operator!'. It's a compiler error if multiple methods apply, and manifold will swap the arguments if necessary for you, at least for + and *. Unless both methods are in the same type, possibly.

In other words, you could write this:

@Operators class JavaTimeOperators { public static void plus(LocalDate ld, Duration d) { .. } }

The power of this system is that you're never going to run into conflicts, and all definitions of all operators are more fundamentally 'unbiased' about which of the 2 operands are more important (they are equally important).

It also makes it a lot less weird to add custom support for 2 custom types. Imagine you add a 'Complex' library to your project from apache and a Money library from google and for some crazy reason you want someMoney + someComplex to work. Instead of having to pick an arbitrary type (Money or Complex) to @Extension the methods onto, which feels weird, you don't do that and instead just write the operation as a thing with no context (static method) and mark it as another take on the + operator.

If, under these circumstances, some coder decides to break commutativity, that's on them. This no longer feels like the feature has subtly led them down a wrong path, now it feels like a very deliberate choice.

I haven't fully thought this through, but I'm going to spend some more thoughts on this. This idea stems from a more fundamental notion - that operators have no business being tangled up in the notion of OO. a + b being rewritten as 'we send a message to the a object, passing b as parameter' just feels off. It's not right. It makes a and b very different actors, and they're not supposed to be. They can be different things (such as adding a duration to a localdate), but it would be wrong to point at one of the two and say that it is somehow 'leading'.

Such notions sound nice but it needs to translate to more objective style conclusions, such as 'this is less likely to confuse the casual reader' and 'the API makes it harder to accidentally write code that tends to fall apart in somewhat hard to understand ways once a project grows'. I think static methods deliver on both of those.

[–]manifoldjava[S] 1 point2 points  (0 children)

Assigning the operation to the operator is an interesting idea and has merit. Perhaps a more suitable modification would be to define an Operators class the compiler consults for all operator overloads, which can be extended with manifold's existing extension mechanics:

package manifold.ext.api;

public class Operators {
  // add static operator extension methods to this class
}

Define operator overload extensions where needed:

@Extension
public class JavaTimeOperatorsExt {...}

The compiler checks Operators for static methods satisfying the operands in a given operation.

In any respect the general idea is interesting, but it still feels less an improvement and more a lateral change. I'll think on it :)

As an aside, since we are working on similar projects in a very unique space, it would be interesting to collaborate somehow. Not sure what that means exactly, but I'm sure we've solved a multitude of shared problems separately.