3

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*} !

Paŭlo Ebermann
  • 73,284
  • 20
  • 146
  • 210

1 Answers1

3

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.

zamfofex
  • 487
  • 1
  • 4
  • 8
  • Is it good practice to do `next++` like that; is it a violation of the interface to call `next()` again if it previously returned `finished`? – Jonas Berlin Aug 02 '18 at 09:29
  • Very nice explanation :) When you write "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", which type parameter do you then refer to? The type of the reference to null which is undergoing the type narrowing when doing `assert(is Foo null)` ? –  Aug 02 '18 at 09:49
  • 1
    @loldrup, that is true for any type parameter that is bounded by `Anything` (unbounded type parameters are implicitly bounded by `Anything`). In that case, the type parameter was `Foo`. – zamfofex Aug 02 '18 at 11:41
  • @JonasBerlin, I think it depends on the situation. Of course, in that case, OP could have simply written `iterator = gcommands.iterator;` and avoid having to iterate over the sequence manually altogether, but I’m sure there are situations in which `++` can be helpful. Also, since `gcommands` is an immutable sequence, OP’s `next` will always return `finished` after its first occurrence. – zamfofex Aug 02 '18 at 11:49
  • @Zambonifofex I don't understand why my `next` will always return finished after its first occurrence..? And what do you mean by 'first occurrence' - do you mean 'first call to `next`? –  Aug 02 '18 at 11:56
  • I mean that, once it returns `finished`, it’ll (correctly) always return `finished` after that. By “first occurrence”, I meant “first occurrence of `finished`”. – zamfofex Aug 02 '18 at 13:02