Multimethods feature proposal
Brad Markel
knightblue610 at yahoo.com
Fri Jan 21 15:30:01 UTC 2022
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