My nullity concept can handle higher kinded types quite well. No can of worms. I'll show you with some examples:
Taking the standard example from Scala for higher kinded types (see http://www.cs.kuleuven.be/~adriaan/files/higher.pdf): An interface that declares that a certain container type is capable of running the map function. This is Iterable in scala, but as we already have a completely different Iterable in java, let's call our interface 'Mappable' to avoid confusion. This thing technically has two separate type parameters: Not just the parameter of the type it can iterate over (the String in ArrayList<String>), but also the type of container (the ArrayList in ArrayList<String>). I'm going to use assume a BGGA-like shorthand syntax is allowed for declaring anonymous inner classes for a moment. Closures would be better but I don't want to get too far away from real life java syntax, to make sure the example is easy to understand. This is the no-null-typing supported version. T is the type of the elements stored in the container (e.g. String), and C is the container type itself, e.g. ArrayList public interface Mapper<F, T> { public T map(F from); } public interface Mappable<T, C> { public <U> C<U> map(Mapper<T, U> mappingFunction); } public class ArrayList<T> implements Mappable<T, ArrayList> { public <U> ArrayList<U> map(function[T => U] mappingFunction) { ArrayList<U> out = new ArrayList<U>(); for ( T t : this ) out.add(mappingFunction.apply(t)); return out; } } public void usage() { List<String> list = new ArrayList<String>(); list.add("1234"); list.add("10"); Mapper<String, Integer> string2int = { String x: return x == null ? 0 : Integer.parseInt(x); }; List<Integer> ints = list.map(string2int); } Let's new rewrite this using my system to proper null-capable code. We have a boatload of parameters here, so it's quite a complex case: 1. 'T' (the type of the container) could allow nulls, it could deny nulls, or it could be declared to be capable of holding either kind, which means you need to be defensive (never add nulls, but when reading, check for them). 2. The container itself might be allowed to be null, or not, or unknown. 3. The mapper might declare itself to never produce nulls, which should certainly be reflected in the C<U> that is returned from the map method. In the example we'll use the 'non-null is the default' version of the syntax, which means: nothing = non-null question mark = definitely allows null star = either way is acceptable, but as a result we need to be defense (e.g. check for null when reading, never put null in when writing). first the interfaces: public interface Mapper<F*, T*> { public T map(F from); } public interface Mappable<T*, C> { public <U*> C<U> map(Mapper<T, U> mappingFunction); } Almost the same as before, but we have added some *s to signal that we're okay with the contents of our container being null, or with our mapper returning null. We're not okay with the returned container type being legally allowed to hold null; our Mappable interface demands that we return something useful. Note how U's nullity state is carried over from the mapper to the returned container in the map function. If your mapper function says that it will map to something that definitely isn't null, then you'll get a container out that also says that its elements can never contain null. Just as we want. Now the arraylist code: public class ArrayList<T*> implements Mappable<T, ArrayList> { public <U*> ArrayList<U> map(Mapper<T, U> mappingFunction) { ArrayList<U> out = new ArrayList<U>(); for ( T t : this ) out.add(mappingFunction.apply(t)); return out; } } Exactly the same, other than 2 stars. Some important points though: This function MUST return something; if at any point it had 'return null;' anywhere in its code, that would have been a compile time error. The burden of assessing nullity status of the t's is not relevant to the arraylist, that's a worry for the mapping function. The fact that this method doesn't need to have any syntax to deal with it is thus a very important property: null troubles don't usually infect everything, just the point where it actually matters. It seems like the mappingFunction might return null (after all, U says it may or may not allow this), and thus that the out.add() call might add null, *BUT*, the generics type of ArrayList 'out' is the exact same generics parameter as mappingFunction's return type. The compiler can also see this, and thus, it's allowed. public void usage() { Mapper<String*, Integer> string2int = { String* x: return x == null ? 0 : Integer.parseInt(x); }; List<String?> list = new ArrayList<String?>(); list.add("1234"); list.add(null); list.add("-5"); List<Integer> ints = list.map(string2int); List<String> nonNullList = new ArrayList<String>(); nonNullList.add("5678"); nonNullList.add("-10"); List<Integer> ints 2 = nonNullList.map(string2int); } In the above example, the string2int mapper is capable of handling nulls, but it does not rely on the input argument being null-capable (not even relevant as it only reads, hence the *). This is a good thing, because if it hadn't declared itself to be null-capable, it would not be a legal argument to list.map (but it would have been just fine for nonNullList.map, of course). The null check inside is not just to return the right thing, but it is in fact mandatory, if we assume for a moment that the core libraries have been updated and Integer.parseInt only accepts a String (and not a String?). Just returning Integer.parseInt(x) would have been a compile time error (the error would be: "You forgot to null-check x."). Also note that our null-capable function is also legal to pass into our nonNull list, but only because T is read-restricted. If we had used a ? there, that wouldn't have been legal. Inferring that in this particular case, '?' is just as good as '*' isn't hard but that kind of inference is not traditionally something java has ever done, so I haven't done it here either. For scala it would make perfect sense to just infer this and make * and ? syntactically equivalent for that method. (Things would have been different if it was a List<T*> vs. a List<T?>, where the closure calls list.add(null). Or if the T type was 'exported' by e.g. creating a new List<T> in the code. Then the inference engine would obviously no longer infer that ? was just as good as *). > Is there a way we can specify nullibility at definition site > instead of usage site? Just like generics, you have to define in both places. > Or is that a bad idea? How does this play > with type inference? Just like generics, you can infer quite a lot if you so choose. You can also avoid going overboard. Heck, you can go so far as to infer that in our mappingFunction above, the code does a null check and only reads, so the compiler can infer that String* is the more useful type there instead of either String or String?. > > I'm not literally looking for the answers to all those questions. I'm > just saying that they all need thought, and design, and > experimentation. > Sure. Which language change doesn't? What's your point, other than this 'duh' moment? > If it's as easy to do as you think it is then the Scala team will be > happy to look at a patch. That bug report I filed hasn't seen any action for months. Not a word. Therefore, the onus of proving that you're willing to accept community patches from people not intricately involved in the scala community is now on them and no longer on me. I also have no real interest in doing this because I have abandoned scala for the time being. I'd rather write up a JSR. --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "The Java Posse" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/javaposse?hl=en -~----------~----~----~----~------~----~------~--~---
