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 else operator:
next() => gcommands[index++] else finished;
Or arguably even better, the getOrDefault method:
next() => gcommands.getOrDefault(index++, finished);
The reason List can return null is because of its narrowing assertion:
assert (is Element null);
As any type‐narrowing assertion, the type of null is narrowed from Null to Null&Element following 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 object declaration), namely, null.
It’s also interesting to note that it’s the type of the reference to null that gets narrowed to Null&Element, and not the Null type itself that changes supertypes.
That is, if you have an expression of type Null, it still won’t be assignable to Element.
For example, consider the following declaration:
Foo foo<Foo>()
{
Null n = null;
assert(is Foo null);
// return n; // Doesn’t work.
return null; // Works.
}
Here, n is of type Null, and null is of type Null&Foo. The later is assignable to Foo and 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 to Nothing) is that the type parameter itself is unrelated to the Object and Null types, being in its own branch in the type hierarchy below Anything.
That is because the type parameter may be realized at runtime as either Anything, Null (or \Inull), Object, or any subtype of Object.
In reality, the getElement method in List behaves the same as this variation:
Element getElement(Integer index)
{
assert(is Element element = getFromFirst(index)).
return element;
}
But the version in the language module is more performant, since exists is faster than is Element. The version in the language module only performs slow runtime type checking when the list contains null elements.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With