Multimethods feature proposal
Brad Markel
knightblue610 at yahoo.com
Fri Jan 21 16:58:24 UTC 2022
>For the first part, if we suppose we want to keep the Java semantics when doing a meultimethod call, there is a >huge issue,
>when you have a multu-methods like
> void foo(Object o, String s) and
> void foo(String s, Object o)
>and dynamically both arguments are String, which method you should call ?
>There are another issue with null but it's manageable.
Thank you for your feedback! I think there may be a way out of the first problem.
In the case of sealed classes, there's a finite number of cases, and the proposal would force you to implement all combinations of the parameters thereof. So, in the case of Shape:
interface Shape {
@Multimethod
Shape collideTwo(Shape shape1, Shape shape2, double angle);
}
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) { ... }
}
If you pass in a Square:
collideTwo(square, circle, 10.0);
collideTwo(circle, square, 10.0);
If you tried this:
collideTwo(triangle, circle, 10.0);
//error - Triangle is not in the permits clause of sealed class Shape
And if you tried this:
class Rectangle {
@Multimethod
Shape collideTwo(Circle shape1, Circle shape2, double angle) { ... }
@Multimethod
Shape collideTwo(Circle shape1, Rectangle shape2, double angle) { ... }
//error - collideTwo(circle, square, angle) is not implemented.
@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) { ... }
}
Both cases are implemented, so you can pick the correct one. We'd have to specify that only sealed classes are eligible as parameters. The trick here is that the number of combinations is finite.
Now, as for nulls, you could specify the null case like you would in the switch:
class Rectangle {
@Multimethod
Shape collideTwo(Circle shape1, Circle shape2, double angle) { ... }
@Multimethod
Shape collideTwo(Circle shape1, Rectangle shape2, double angle) { ... }
//error - collideTwo(circle, square, angle) is not implemented.
@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) { ... }
@Multimethod
Shape collideTwo(Shape nullPointer, Shape nullPointer, double angle) { ... }
}
Thanks,
Brad Markel
On Friday, January 21, 2022, 10:05:46 AM CST, Remi Forax <forax at univ-mlv.fr> wrote:
----- Original Message -----
> From: "Brad Markel" <knightblue610 at yahoo.com>
> To: "amber-dev" <amber-dev at openjdk.java.net>
> Sent: Friday, January 21, 2022 4:30:01 PM
> Subject: Multimethods feature proposal
> 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
Disclaimer, i hold a Phd on multi-emthod implementation in Java.
With multi-methods, you have two parts, one is how to compile them safely the other is how to implement them efficiently.
For the first part, if we suppose we want to keep the Java semantics when doing a meultimethod call, there is a huge issue,
when you have a multu-methods like
void foo(Object o, String s) and
void foo(String s, Object o)
and dynamically both arguments are String, which method you should call ?
There are another issue with null but it's manageable.
This preclude to introduce multi-methods directly in the language.
The second part is far easier, Java 7 introduces invokedynamic and method handles,
so we can simulate a muti-dispatch quite easily, as an example, you can use the library Macro [1] for that,
by declaring that all parameters are CONSTANT_CLASS.polymorphic(), the actuel code is left as an exercise :)
About the relation between switch and multi-methods, a switch on a tuple is a multi-method.
Let suppose we have the record
record ShapePair(Shape shape1, Shape shape2) { }
JEP 405 [2], introduces record pattern (my hope is to have it available for Java 19),
with that one can write
Shape collide(Shape shape1, Shape shape2, double angle) {
return switch (new ShapePair(shape1, shape2)) {
case ShapePair(Circle c1, Circle c2) -> { ... }
case ShapePair(Rectangle r, Circle c) -> { ... }
...
};
}
which is i believe what you want.
regards,
Rémi
[1] https://github.com/forax/macro
[2] https://openjdk.java.net/jeps/405
More information about the amber-dev
mailing list