Multimethods feature proposal
Remi Forax
forax at univ-mlv.fr
Fri Jan 21 16:05:39 UTC 2022
----- 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