You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2020/05/18 09:43:49 UTC

[groovy] branch GROOVY_3_0_X updated: initial cut of doco for dynamic method selection

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/GROOVY_3_0_X by this push:
     new 8bae4ae  initial cut of doco for dynamic method selection
8bae4ae is described below

commit 8bae4ae52ad912d072ab387c4fa2169e8d73f8ef
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon May 18 19:42:45 2020 +1000

    initial cut of doco for dynamic method selection
---
 src/spec/doc/core-object-orientation.adoc          | 108 ++++++++++++++++++++-
 src/spec/test/objectorientation/MethodsTest.groovy |  92 ++++++++++++++++++
 2 files changed, 199 insertions(+), 1 deletion(-)

diff --git a/src/spec/doc/core-object-orientation.adoc b/src/spec/doc/core-object-orientation.adoc
index 5121796..ffb6ba3 100644
--- a/src/spec/doc/core-object-orientation.adoc
+++ b/src/spec/doc/core-object-orientation.adoc
@@ -438,7 +438,113 @@ include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=va
 
 ==== Method selection algorithm
 
-(TBD)
+Dynamic Groovy supports https://en.wikipedia.org/wiki/Multiple_dispatch[multiple dispatch] (aka multimethods).
+When calling a method, the actual method invoked is determined
+dynamically based on the run-time type of methods arguments.
+First the method name and number of arguments will be considered (including allowance for varargs),
+and then the type of each argument.
+Consider the following method definitions:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_methods,indent=0]
+----
+
+Perhaps as expected, calling `method` with `String` and `Integer` parameters,
+invokes our third method definition.
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=call_single_method,indent=0]
+----
+
+Of more interest here is when the types are not known at compile time.
+Perhaps the arguments are declared to be of type `Object` (a list of such objects in our case).
+Java would determine that the `method(Object, Object)` variant would be selected in all
+cases (unless casts were used) but as can be seen in the following example, Groovy uses the runtime type
+and will invoke each of our methods once (and normally, no casting is needed):
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=call_multi_methods,indent=0]
+----
+
+For each of the first two of our three method invocations an exact match of argument types was found.
+For the third invocation, an exact match of `method(Integer, Integer)` wasn't found but `method(Object, Object)`
+is still valid and will be selected.
+
+Method selection then is about finding the _closest fit_ from valid method candidates which have compatible
+parameter types.
+So, `method(Object, Object)` is also valid for the first two invocations but is not as close a match
+as the variants where types exactly match.
+To determine the closest fit, the runtime has a notion of the _distance_ an actual argument
+type is away from the declared parameter type and tries to minimise the total distance across all parameters.
+
+The following table illustrates some factors which affect the distance calculation.
+
+[cols="1,1" options="header"]
+|====
+| Aspect
+| Example
+
+| Directly implemented interfaces match more closely than ones from further up the inheritance hierarchy.
+a| Given these interface and method definitions:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interfaces,indent=0]
+----
+
+The directly implemented interface will match:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interfaces_usage,indent=0]
+----
+
+| An Object array is preferred over an Object.
+a|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=object_array_over_object,indent=0]
+----
+
+| Non-vararg variants are favored over vararg variants.
+a|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=non_varargs_over_vararg,indent=0]
+----
+
+| If two vararg variants are applicable, the one which uses the minimum number of vararg arguments is preferred.
+a|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=minimal_varargs,indent=0]
+----
+
+| Interfaces are preferred over super classes.
+a|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_distance_interface_over_super,indent=0]
+----
+|====
+
+In the case where two variants have exactly the same distance, this is deemed ambiguous and will cause a runtime exception:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_ambiguous,indent=0]
+----
+
+Casting can be used to select the desired method:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/objectorientation/MethodsTest.groovy[tags=multi_method_ambiguous_cast,indent=0]
+----
+
 
 ==== Exception declaration
 
diff --git a/src/spec/test/objectorientation/MethodsTest.groovy b/src/spec/test/objectorientation/MethodsTest.groovy
index a0278bb..12f08fd 100644
--- a/src/spec/test/objectorientation/MethodsTest.groovy
+++ b/src/spec/test/objectorientation/MethodsTest.groovy
@@ -162,4 +162,96 @@ class MethodsTest extends GroovyTestCase {
         '''
     }
 
+    void testMultiMethods() {
+        assertScript '''
+            // tag::multi_methods[]
+            def method(Object o1, Object o2) { 'o/o' }
+            def method(Integer i, String  s) { 'i/s' }
+            def method(String  s, Integer i) { 's/i' }
+            // end::multi_methods[]
+
+            // tag::call_single_method[]
+            assert method('foo', 42) == 's/i'
+            // end::call_single_method[]
+
+            // tag::call_multi_methods[]
+            List<List<Object>> pairs = [['foo', 1], [2, 'bar'], [3, 4]]
+            assert pairs.collect { a, b -> method(a, b) } == ['s/i', 'i/s', 'o/o']
+            // end::call_multi_methods[]
+        '''
+
+        assertScript '''
+            import static groovy.test.GroovyAssert.shouldFail
+            // tag::multi_method_ambiguous[]
+            def method(Date d, Object o) { 'd/o' }
+            def method(Object o, String s) { 'o/s' }
+
+            def ex = shouldFail {
+                println method(new Date(), 'baz')
+            }
+            assert ex.message.contains('Ambiguous method overloading')
+            // end::multi_method_ambiguous[]
+            // tag::multi_method_ambiguous_cast[]
+            assert method(new Date(), (Object)'baz') == 'd/o'
+            assert method((Object)new Date(), 'baz') == 'o/s'
+            // end::multi_method_ambiguous_cast[]
+        '''
+
+        assertScript '''
+            // tag::multi_method_distance_interfaces[]
+            interface I1 {}
+            interface I2 extends I1 {}
+            interface I3 {}
+            class Clazz implements I3, I2 {}
+
+            def method(I1 i1) { 'I1' }
+            def method(I3 i3) { 'I3' }
+            // end::multi_method_distance_interfaces[]
+
+            // tag::multi_method_distance_interfaces_usage[]
+            assert method(new Clazz()) == 'I3'
+            // end::multi_method_distance_interfaces_usage[]
+        '''
+
+        assertScript '''
+            // tag::non_varargs_over_vararg[]
+            def method(String s, Object... vargs) { 'vararg' }
+            def method(String s) { 'non-vararg' }
+
+            assert method('foo') == 'non-vararg'
+            // end::non_varargs_over_vararg[]
+        '''
+
+        assertScript '''
+            // tag::minimal_varargs[]
+            def method(String s, Object... vargs) { 'two vargs' }
+            def method(String s, Integer i, Object... vargs) { 'one varg' }
+
+            assert method('foo', 35, new Date()) == 'one varg'
+            // end::minimal_varargs[]
+        '''
+
+        assertScript '''
+            // tag::object_array_over_object[]
+            def method(Object[] arg) { 'array' }
+            def method(Object arg) { 'object' }
+
+            assert method([] as Object[]) == 'array'
+            // end::object_array_over_object[]
+        '''
+
+        assertScript '''
+            // tag::multi_method_distance_interface_over_super[]
+            interface I {}
+            class Base {}
+            class Child extends Base implements I {}
+
+            def method(Base b) { 'superclass' }
+            def method(I i) { 'interface' }
+
+            assert method(new Child()) == 'interface'
+            // end::multi_method_distance_interface_over_super[]
+        '''
+    }
+
 }