Notes on implementing concise calls to constructors with type parameters
Peter Levart
peter.levart at gmail.com
Thu May 21 08:22:10 PDT 2009
Answers following in-line...
On Thu, May 21, 2009 at 2:18 PM, Howard Lovatt <howard.lovatt at iee.org>wrote:
> Hi Peter,
>
> 2009/5/21 Peter Levart <peter.levart at gmail.com>:
> > Hello Howard,
> >
> > What Neal and others are trying to point out is that your "textual
> > substitution algorithm" is not defined. The name "textual" implies that
> it
> > should be simple. But the right thing to do is not simple at all.
>
> I suspect that we have more of a philosophical difference rather than
> a technical one. I think it is a mistake to choose complicated systems
> that few understand; you have to keep it simple. Much of Java is a
> simplification of C++ and this simplification is the reason for the
> success of Java. An example of a feature of Java that has received
> much negative comment is variance, to me this is an example of a
> system that though powerful is too complicated. The reason for people
> wanting to have type declarations on the left rather than the var
> (auto) type construct is for this very reason of simplicity, so it
> makes no sense to me to use a complicated system when the aim is
> simplicity. In summary the number one goal for me is simplicity,
> rather than ultimate power.
... and you forgot "consistency". That means features should be orthogonal.
If it works that way in one context it should work the same way in all
contexts. Consistency increases simplicity. What you propose is something
that would work differently for constructors than for example inference
works in generic methods. This would not achieve your ultimate goal.
>
> It doesn't matter how hard you try with type inference it would seem
> that some part of it will fail. The following example from Peter Ahe
> blog demonstrates this:
>
> static <T> void merge( List<T> l1, List<T> l2 ) {}
>
> static void test(List<? extends Object> list) { merge(list, list); }
>
> Fails with an obscure error message:
>
> <T>merge(java.util.List<T>,java.util.List<T>) in listtypesafety.Main
> cannot be applied to (java.util.List<capture of ? extends
> java.lang.Object>,java.util.List<capture of ? extends
> java.lang.Object>)
>
> Yet to a human reader it is obvious that it is type sound. Now
> unfortunately you have a failure for no apparent reason and a system
> that is so complicated you cannot work out why and therefore can't
> work out how to fix the problem. (I do know why it fails, but you have
> to admit it is not obvious.)
Not obvious is only to someone that assumes the problem domain is not
complicated. In order to understand the failure of the above compilation,
you have to consider the whole problem domain. The compiler has to consider
the whole problem domain because it must NEVER produce wrong result. Would
it allow the above call, then it would also allow for example this:
public class Test
{
<T> void merge(List<T> l1, List<T> l2)
{
// ...
}
static final List<String> STRINGS = Arrays.asList("xx", "yy");
static final List<Integer> INTEGERS = Arrays.asList(1, 2, 3);
volatile List<?> list = STRINGS;
void test()
{
new Thread()
{
public void run()
{
list = INTEGERS;
}
}.start();
merge(list, list); // it can happen that we merge STRINGS with
INTEGERS !!!
}
}
In order for the compiler to allow Peter Ahe's example and not allow the
exploit above, it would have to even further "complicate" the algorithm and
include special cases for local or final variables. This might be an idea
for a project COIN proposal if it bothered many people. But no such proposal
was seen on this list, so we could assume it doesn't bother to many people.
What we have now is "simplified" handling that does not consider valid
special cases but throws them together with invalid cases. Type inference
algorithm is in this respect to simple, not to complicated. I agree that
diagnostic message could be better though.
>
> > Have you
> > looked at the 3rd JLS section 15.12.2.7 ?
>
> Yes
>
> > To just scratch the surface, take
> > the following example:
> >
> > public interface IntA<X, Y> { ... }
> >
> > public class ClsA<Z> implements IntA<String, Z> { ... }
> >
> > public class ClsB<Z> implements IntA<Z, Integer> { ... }
> >
> > ...
> > public void doIt(IntA<String, Integer> param) { ... }
> > ...
> > // what should "textual substitution" do in the following 2 cases:
> > anInstance.doIt(new ClsA());
> > // vs.
> > anInstance.doIt(new ClsB());
>
> For an example like this I would use raw types, to remain compatible
> with existing code. This is the same answer I gave for multiply
> constrained types.
>
> Also note the following slight variation currently fails:
>
> static class ClsA<Z> implements IntA<String, Z> {
> static <Z> ClsA<Z> instance() { return new ClsA<Z>(); }
> }
>
> static class ClsB<Z> implements IntA<Z, Integer> {
> static <Z> ClsB<Z> instance() { return new ClsB<Z>(); }
> }
>
> static void inferenceTest() {
> doIt( ClsA.instance() );
> doIt( ClsB.instance() );
> }
>
> Which shows what a difficult example this is.
>
Yes, but the following works:
IntA<String, Integer> a = ClsA.instance();
IntA<String, Integer> b = ClsB.instance();
... it's just a matter of further "complicating" the inference algorithm in
combination with overloaded methods. I read on this list that today the
theory can do it.
> Back on my philosophical point, I don't mind the inference resulting
> in a raw type (no inference) for an example like this. The chance of
> something like this occurring in real code is slim and if you are
> really worried about the types then you can specify them. For me it is
> much more important that examples like:
>
> // Translation // Original
> Ex e___ = new Ex(); // Ex
> e___ = new();
> Ex e__s = new Ex<String>(); // Ex e__s
> = new<String>();
> Ex e_e_ = new Ex(); // Ex
> e_e_ = new Ex();
> Ex e_es = new Ex<String>(); // Ex e_es
> = new Ex<String>();
> Ex<String> es__ = new Ex<String>(); // Ex<String> es__ = new();
> Ex<String> es_s = new Ex<String>(); // Ex<String> es_s = new<String>();
> Ex<String> ese_ = new Ex<String>(); // Ex<String> ese_ = new Ex();
> Ex<String> eses = new Ex<String>(); // Ex<String> eses = new
> Ex<String>();
>
> Work well and are easily understood.
>
It might work well for straightforward cases but will be unusable for many
others. And It might bring a lot of surprises. For example:
public class InferenceTest
{
interface IntA<X, Y>
{
}
static class ClsA<X, Y> implements IntA<Y, X> // note the reversal of X
& Y
{
}
static void inferenceTest()
{
IntA<String, String> a = new ClsA(); // inferred as new
ClsA<String, String>();
IntA<Integer, Integer> b = new ClsA(); // inferred as new ClsA<
Integer, Integer>();
IntA<String, Integer> c = new ClsA(); // produces raw type new
ClsA(); ???
}
}
I think the "textual substitution and fall back to raw type if it doesn't
compile" is not a serious algorithm for any compiler or language. It is
simply not consistent. I think we can do better. Java should only get the
best.
Regards, Peter
> Cheers,
>
> -- Howard.
>
> >
> > Peter.
> >
> >
> > On Mon, May 18, 2009 at 10:13 AM, Howard Lovatt <howard.lovatt at iee.org>
> > wrote:
> >>
> >> Hi Neal,
> >>
> >> Yes I should have spelled this out, it is one of those things obvious
> >> to the author but not to the reader. I would propose that given more
> >> than one method of the same name, method in example below, with the
> >> general declaration for each following the syntax:
> >>
> >> ... method ( typeLHS [<genericParametersLHS>] name , ... ) { ... }
> >>
> >> and given the call:
> >>
> >> ... method ( new [typeRHS] [<genericParametersRHS>] ( ... ) , ... );
> >>
> >> Then a three step procedure would be used:
> >>
> >> 1. If typeRHS is absent assume for step 2 type Void, where Void is the
> >> bottom type (in Java 7 this may be called Null but I chose Void to be
> >> consistent with the InvokeDynamic proposal)
> >>
> >> 2. Resolve method as normal and hence find typeLHS and if specified
> >> genericParametersLHS also
> >>
> >> 3. Apply textual substitution algorithm, i.e. If typeRHS is absent
> >> substitute typeLHS and if genericParametersRHS is absent and
> >> genericParametersLHS is present then substitute genericParametersLHS
> >>
> >> For example, given two methods:
> >>
> >> void method( Object notUsed ) { out.println( "Object" ); }
> >> void method( String notUsed ) { out.println( "String" ); }
> >>
> >> The call:
> >>
> >> method( new() );
> >>
> >> would print String because it would be translated by the textual
> >> substitution algorithm into method( new String() ), i.e. the same
> >> behaviour as method( null ) because null has type Void (bottom).
> >>
> >> Does that add sufficient clarity? I would be really interested if you
> >> can see a problem with the technique?
> >>
> >> If not the method maybe a good solution as it has many desirable
> >> characteristics (as listed in previous posts in this thread). I also
> >> note that many people would like a var or auto style declaration and
> >> maybe this proposal of text substitution can satisfy more people than
> >> other proposals, since it retains LHS types but saves re-specification
> >> of type, generic type, and doesn't require a diamond operator on the
> >> RHS.
> >>
> >> -- Howard.
> >>
> >>
> >
> >
> > ______________________________________________________________________
> > This email has been scanned by the MessageLabs Email Security System.
> > For more information please visit http://www.messagelabs.com/email
> > ______________________________________________________________________
> >
>
>
>
> --
> -- Howard.
>
More information about the coin-dev
mailing list