JDK-8194177 : JShell: scratch variables -- retrieve run-time type and combine with compile-time type
  • Type: Enhancement
  • Component: tools
  • Sub-Component: jshell
  • Affected Version: 9,10
  • Priority: P3
  • Status: Resolved
  • Resolution: Won't Fix
  • Submitted: 2017-12-27
  • Updated: 2018-02-22
  • Resolved: 2018-02-22
Related Reports
Relates :  
Description
I was surprised to learn this behavior today; that the static type of a $-var is not narrowed to its dynamic type.  Which means:

jshell> Object m() { return ""; }
|  created method m()

jshell> m()
$4 ==> ""

jshell> $4.length()
|  Error:
|  cannot find symbol
|    symbol:   method length()
|  $4.length()
|  ^-------^


Since it just told me that $4 is "", I would expect its type to be String, not Object, and that I could just ask for its length. (Especially as we don't even print out the type when we say $4 ==> "", so the user has no idea what the type is, and might reasonably expect it to be String.)

------

All syntax and semantics are Java syntax and semantics. Automatically generated variables are no different.

You can see the type of variables (including automatically generated variables) with verbose feedback mode,  in /var, or by using a different result format, or if using the API.

They are prefixed to appropriate expressions, but are otherwise processed and treated like any other variable (they are just normal variables).

The current implementation evaluates this in one pass by turning e into: 

    compile_time_type_of_e $i = e

Changing $i to be the run-time type of e would require three passes and infrastructure for externally hidden snippets, or adding new functionality to the transport layer, probably moving scratch variables creation to the back-end.

----

I understand how these tools work, and I still had to scratch my head for a minute before I realized what was going on.  I did:

jsh> foo()

$1 = Foo[a=1, b=2]

jsh> $1.a()
���error

and thought ��� WTF?  Clearly it���s a Foo.  Surely it���s a Foo, right?  

jsh> $1.getClass()

$2 = Foo

yep, It���s a Foo.  Yes, I quickly figured out that the static type of $1 was getting in the way, I cast it to (Foo) and went on my way.  (And then the same thing bit me about a dozen more times in the same session.)  For a use who has a weaker understanding of static vs dynamic type, the explanation and workaround may not occur to them, and we risk violating the principle of least astonishment.  

But more fundamentally, one of the core use cases for jshell is interactive exploration of APIs.  Many APIs are strongly typed, so jshell is great there.  But some APIs (e.g., reflection, many XML/Json APIs, etc) are weakly typed, serving up Object.  And users often want to navigate their way through something ��� ask reflection to evaluate a field, and then do something on that result.  JShell helpfully puts the result in a var for you ��� and even tells you its a Foo (via toString), but then doesn���t let you interact with it as a Foo.  

So, what could we do to help the user here?  Well, we could print out the type of $1, at least giving a clue to the user what���s going on ��� but 99% of the time, that would just be noise, so I don���t think that���s a great solution.  The creation of $i variables is entirely outside the language, so jshell has some latitude here in their treatment.  

We could refine the static type of $i to match its dynamic type, as suggested.  (This should be paired with making $i final, which is probably a good idea anyway.)  

We could provide some tool-based help for replacing $i with ((D) $i) on the input line (say, hitting ctrl-space a second time would replace $i with ((D) $i) and do completion on it.)  This would be entirely outside the compilation pipeline and back-end, and strictly about allowing the user to say what they mean more easily.)  

----

I've also run into cases were having it be runtime-typed would be handy.

In preparation for filing an enhancement request, I thought more deeply about the implementation, and realized that we'd run into the fact that generics are not reified.

For example (using verbose feedback mode so that types show):

jshell> Map<String, List<Double>> map = new HashMap<>()
map ==> {}
|  created variable map : Map<String, List<Double>>

jshell> new ArrayList<Double>()
$2 ==> []
|  created scratch variable $2 : ArrayList<Double>

jshell> $2.getClass()
$3 ==> class java.util.ArrayList
|  created scratch variable $3 : Class<? extends ArrayList>

jshell> $2.add(2.3)
$4 ==> true
|  created scratch variable $4 : boolean

jshell> map.put("foo", $2)
$5 ==> null
|  created scratch variable $5 : List<Double>

jshell> map.get("foo").getClass()
$6 ==> class java.util.ArrayList
|  created scratch variable $6 : Class<? extends List>

We could attempt to combine the generic information (and information when the value is null) with the runtime class information.....

----

The inference procedure used by diamond may be appropriate.  Say the generic type is List<String> and the dyn type is ArrayList.  With diamond we correctly infer the params of arraylist from the generic super type. 





Comments
See Dan's comment
22-02-2018

The problem of combining runtime and static information can be generalized to the problem of simplifying intersection types. See the discussion about types like 'ArrayList<?> & Iterable<String>' in JDK-8039222. Pattern matching will have a similar problem.
05-01-2018