I'm trying to create a class 'Gprogram' that satisfies the interface Iterable (such that I can iterate over the Gcommand's in my Gprogram). However, I can only make it iterable with the type Iterable<Gcommand|Null,Nothing> where I would rather have Iterable<Gcommand,Nothing>.
The problem is: when I try to use Iterable<Gcommand,Nothing>, I get this error:
specified expression must be assignable to declared type of 'next' of 'Iterator' with strict null checking: 'Null|Gcommand|finished' is not assignable to 'Gcommand|Finished' (the assigned type contains 'null')
this error refers to this code snippet:
next() => index>=size
then finished
else gcommands[index++];
which is taken from the full implementation here:
shared class Gprogram( Gcommand+ gcommands ) satisfies Iterable<Gcommand|Null,Nothing> {
shared actual default Iterator<Gcommand|Null> iterator() {
if (gcommands.size > 0) {
return object
satisfies Iterator<Gcommand|Null> {
variable Integer index = 0;
value size = gcommands.size;
next() => index>=size
then finished
else gcommands[index++];
string => gcommands.string + ".iterator()";
};
}
else {
return emptyIterator;
}
}
}
The issue seems to me to be that the type checker can't realize that the next method can't ever return null (realising so would involve reasoning about integer values, which the type checker can't do). Thus, all hope is out, right..?
One nagging question remains: How does List manage to do what I can't?? Let's have a look at the implementation of iteratorfor the List class:
shared actual default Iterator<Element> iterator() {
if (size>0) {
return object
satisfies Iterator<Element> {
variable Integer index = 0;
value size = outer.size;
next() => index>=size
then finished
else getElement(index++);
string => outer.string + ".iterator()";
};
}
else {
return emptyIterator;
}
}
where the getElement function looks like this:
Element getElement(Integer index) {
if (exists element = getFromFirst(index)) {
return element;
}
else {
assert (is Element null);
return null;
}
}
( full source code )
It can be seen that getElement is very much capable of returning Null values. But how can it then be that List's satisfaction of the Iterable interface doesn't mention Nulls, like my implementation is forced to do? The relevant 'satisfies'-statement resides in List's supertype, Collection. See here:
shared interface Collection<out Element=Anything>
satisfies {Element*} {
( full source code )
Look, ma! No {Element|Null*} !
Yes, indeed, the Ceylon compiler doesn’t reason about integers when checking for the nullability of the lookup expression (the square brackets), since Ceylon is not a dependently typed language.
A better (actually working) approach would be to use the
elseoperator:Or arguably even better, the
getOrDefaultmethod:The reason
Listcan returnnullis because of its narrowing assertion:As any type‐narrowing assertion, the type of
nullis narrowed fromNulltoNull&Elementfollowing the assertion.It’s interesting, however, to note that, unlike your everyday narrowing condition, this one affects not a local value, but an anonymous class (i.e. an
objectdeclaration), namely,null.It’s also interesting to note that it’s the type of the reference to
nullthat gets narrowed toNull&Element, and not theNulltype itself that changes supertypes.That is, if you have an expression of type
Null, it still won’t be assignable toElement.For example, consider the following declaration:
Here,
nis of typeNull, andnullis of typeNull&Foo. The later is assignable toFooand can be returned without errors. However, the first is not, and produces an error.This is due to the inability to narrow type’s supertypes and subtypes (as opposed to narrowing value’s types, which is already possible).
The reason narrowing the type of
null“works” (as opposed to simplifying toNothing) is that the type parameter itself is unrelated to theObjectandNulltypes, being in its own branch in the type hierarchy belowAnything.That is because the type parameter may be realized at runtime as either
Anything,Null(or\Inull),Object, or any subtype ofObject.In reality, the
getElementmethod inListbehaves the same as this variation:But the version in the language module is more performant, since
existsis faster thanis Element. The version in the language module only performs slow runtime type checking when the list containsnullelements.