Multimethods feature proposal

Brian Goetz brian.goetz at oracle.com
Fri Jan 21 17:12:53 UTC 2022


In theory, there's nothing wrong with multiple dispatch.  But I think 
it's a poor candidate for layering atop of Java today; having two kinds 
of instance dispatch, which depends on how the invoked method was 
declared, adds a lot of complexity not only to the language, but also 
asks users to reason about something complex and new in addition to the 
dispatch rules they've already internalized (and may move some type 
checking from compile time to run time.)  We'd likely also have to have 
different rules for allowable overloads of multi methods than we do for 
single-dispatch methods (e.g., maybe we need a "meet" rule).  If 
multiple dispatch were the *only* form of dispatch, this would be 
different, but we picked our dispatch-horse 25 years ago.  So, the cost 
is large, both for the language, and for the users.

So, what's the payback?  I think its more limited than it might first 
appear.

It is a not-too-uncommon reaction, when someone first sees pattern 
matching, to say "what do we need that for, we have virtual methods."  
What this misses is that some operations are indeed important enough to 
put in our object hierarchy, but some operations are purely ad-hoc 
polymorphism.  And there's a range in between.

Multi-method dispatch addresses a part of the intermediate range between 
"just put it in the hierarchy" and "completely ad-hoc", but only a 
part.  So all this complexity is in aid of improving only a small slice 
of the things we might express by pattern switches.

I also don't think most Java developers will think programming in the 
model you propose seems "better"; I think they'll see it as just more 
ceremony, and so are more likely to write the "duplicative" code you are 
concerned about anyway.




On 1/21/2022 10:30 AM, Brad Markel wrote:
> Greetings,
> First, let me start off by apologizing if I'm emailing the wrong mailing list.  I understand that the JEP process suggests emailing feature requests to another list, but since this is so closely related to the Amber development of pattern matching with sealed classes, I felt it wise to run this by the team before submitting a JEP to that list.
>
> Consider the use of a sealed type in a switch expression as envisioned by the current second preview feature in JDK 17:
>
>    Shape rotate(Shape shape, double angle) {
>        return switch (shape) {   // pattern matching switch
>            case Circle c    -> c;
>            case Rectangle r -> r.rotate(angle);
>            case Square s    -> s.rotate(angle);
>            // no default needed!
>        }
>    }
>
> This code, essentially, calls another method based on the type of its parameter as well as the type of the caller.  This is double-dispatch code, but it's written with too much ceremony; putting code in blocks of a switch statement like this encourages users to add complex logic to the switch that should be factored out:
>
>    Shape rotate(Shape shape, double angle) {
>        return switch (shape) {   // pattern matching switch
>            case Circle c    -> {
>              c.setColor("RED");
>              c.setLine("DASHED");
>              //Some other processing code...
>              yield c;
>            }
>            case Rectangle r -> {
>              r.setColor("RED");
>              r.setLine("DASHED");
>              yield r.rotate(angle);
>            }
>            case Square s    -> {
>              s.setColor("RED");
>              s.setLine("DASHED");
>              yield s.rotate(angle);
>            }
>            // no default needed!
>        }
>    }
>
> The original intent of inheritance is to bind code like this to the type, rather than adding potentially risky branching code.  This can get worse if more than one parameter is passed in:
>
>    Shape collide(Shape shape1, Shape shape2, double angle) {
>        return switch (shape1) {   // pattern matching switch
>            case Circle c    -> {
>              switch (shape2) {
>                case Circle c2 -> {
>                  //Some pre-processing code...
>                  yield findAreaOfCollisionForCircle(c, c2);
>                }
>                case Rectangle r2 -> {
>                  //Some pre-processing code....
>                  yield findAreaOfCollisionForRectangle(c, r2);
>                }
>                case Square s2 -> {
>                  //Some pre-processing code....
>                  yield findAreaOfCollisionForSquare(c, s2);
>                }
>              }
>            }
>            case Rectangle r -> {
>              switch (shape2) {
>                case Circle c2 -> {
>                  //Some pre-processing code...
>                  yield findAreaOfCollisionForCircle(r, c2);
>                }
>                case Rectangle r2 -> {
>                  //Some pre-processing code....
>                  yield findAreaOfCollisionForRectangle(r, r2);
>                }
>                case Square s2 -> {
>                  //Some pre-processing code....
>                  yield findAreaOfCollisionForSquare(r, s2);
>                }
>              }
>            }
>            case Square s    -> {
>              //and so on....
>            }
>            // no default needed!
>        }
>    }
>
> Instead, the proposal would be that the compiler generates this code instead, and allow a multimethod annotation at the interface level:
>
>    interface Shape {
>      @Multimethod
>      Shape collide(Shape shape, double angle);
>    }
>
>    class Rectangle {
>      @Multimethod
>      Shape collide(Circle shape, double angle) { ... }
>      @Multimethod
>      multi Shape collide(Rectangle shape, double angle) { ... }
>      @Multimethod
>      multi Shape collide(Square square, double angle) { ... }
>    }
>
>    class Rectangle {
>      @Multimethod
>      Shape collide(Circle shape, double angle) { ... }
>      @Multimethod
>      Shape collide(Rectangle shape, double angle) { ... }
>      @Multimethod
>      Shape collide(Square square, double angle) { ... }
>    }
>
>    class Square {
>      @Multimethod
>      Shape collide(Circle shape, double angle) { ... }
>      @Multimethod
>      Shape collide(Rectangle shape, double angle) { ... }
>      @Multimethod
>      Shape collide(Square square, double angle) { ... }
>      // no default needed!
>    }
>
> This reduces the confusion of adding switch statements to auxiliary classes where class behavior depends on the interaction between two or more types.  This would extend to multiple parameters as well:
>
>    interface Shape {
>      @Multimethod
>      Shape collideTwo(Shape shape1, Shape shape2, double angle);
>    }
>
>    class Circle {
>      @Multimethod
>      Shape collideTwo(Circle shape1, Circle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Circle shape1, Rectangle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Circle shape1, Square shape2, double angle) { ... }
>
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Circle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Rectangle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Square shape2, double angle) { ... }
>      // ...and so on
>    }
>
>    class Rectangle {
>      @Multimethod
>      Shape collideTwo(Circle shape1, Circle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Circle shape1, Rectangle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Circle shape1, Square shape2, double angle) { ... }
>
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Circle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Rectangle shape2, double angle) { ... }
>      @Multimethod
>      Shape collideTwo(Rectangle shape1, Square shape2, double angle) { ... }
>    }
>
> Thus, the branching code is reduced, and the functionality is encapsulated into methods, improving readability and reducing testing complexity.  The compiler internally can generate the complex switch statements, which is essentially the equivalent of the above.
> Would such a feature be worthy of consideration?
> Thanks for your time,Brad Markel
>
>


More information about the amber-dev mailing list