instanceof Number vs has :toNumber

Is this a bug or I don't understand something?

function getConfig(key as PropertyKeyType) as PropertyValueType or Null {
    return Properties.getValue(key);
}

function getConfigNumber(key as PropertyKeyType, defaultValue as Number) as Number {
    var value = getConfig(key);
    if (value instanceof Lang.Number) { // how can I get rid of this if?
        return value;
    }
    if (value instanceof Lang.Boolean) {
        return value ? 1 : 0;
    }
    if (value != null && value has :toNumber) { // this should cover Float, Double, Number, String, Long, Char, Symbol
    value = value.toNumber();
    }
    return value != null ? value as Number : defaultValue;
}

Since null in not instance of Lang.Number and Number has toNumber, I thought that I can get rid of the 1st if. But it doesn't compile (even if I add value != null):
function getConfigNumber(key as PropertyKeyType, defaultValue as Number) as Number {
    var value = getConfig(key);
    if (value != null && value has :toNumber) { // ERROR in this line
        value = value.toNumber();
    }
    if (value instanceof Lang.Boolean) {
        return value ? 1 : 0;
    }
    return value != null ? value as Number : defaultValue;
}
ERROR: Config.mc:109,4: Cannot find symbol 'toNumber' on type 'PolyType<Null or $.Toybox.Application.PropertyKeyType or $.Toybox.Lang.Array<$.Toybox.Application.PropertyValueType> or $.Toybox.Lang.Dictionary<$.Toybox.Application.PropertyKeyType,$.Toybox.Application.PropertyValueType> or $.Toybox.WatchUi.BitmapResource>'.
but if I have any* value instanceof Lang.SOMETHING before that line then it compiles:
*) SOMETHING has to be a possible type in PropertyValueType

function getConfigNumber(key as PropertyKeyType, defaultValue as Number) as Number {
    var value = getConfig(key);
    if (value instanceof Lang.Boolean) {
        return value ? 1 : 0;
    }
    if (value != null && value has :toNumber) { // this should cover Float, Double, Number, String, Long, Char, Symbol
        value = value.toNumber();
    }
    return value != null ? value as Number : defaultValue;
}
  • I don't think has can do the same kind of compile-type type narrowing as instanceof. The type checker just isn't that smart.

    You can file a bug, but I suspect this is by design. (I'm not claiming it's a great design.)

    (Contrast with typescript, whose type-checking is wholly based on "shapes" instead of concrete types, meaning that an object X is considered to be compatible with type Y iff X has all the fields of Y, with compatible types.) i.e. Typescript is "fully" duck-typed whereas Monkey C still has concrete types.

    You could probably cast to Any (which is unsafe imo, but as long as it "works"...)

  • I tried to cast to Any but it doesn't work either.

  • Oh right, my bad, I keep forgetting you can't actually cast to Any lol.

    Well, this works:

    value = (value as Number).toNumber();

    Or even better:

    typedef ConvertibleToNumber as interface {
       function toNumber() as Number;
    };
    ...
    value = (value as ConvertibleToNumber).toNumber();

    Too bad neither alternative actually fulfills the goal of strict type checking... (Meaning if you apply either of these solutions inappropriately, you won't get a compile-time error, because Monkey C doesn't care if you cast to an incompatible type.)

  • I think you missed the point: If I knew it's a Number then I would just do value as Number. But I don't know. This is the code that returns a property as a Number. But it might return from Properties.getValue as PropertyValueType or Null, so my code tries to convert any logical type to Number: if the value has toNumber then use it, if not and it's a boolean then convert it to 0 or 1, if it's anything else (or the toNumber returned null because the value can't be converted to a number) then return the defaultValue.

    So I can't use (value as ConvertibleToNumber).toNumber(), at least not before the if that checks either what I was trying to check: value has :toNumber or if we go with your example then I suppose value instanceof ConvertibleToNumber.

    But the whole point of my change was to a) get rid of instanceof as much as possible because it's lot of code, b) shrink the code as much as possible (thus trying to remove the if (value instanceof Number), because as I imagine the number should also be taken care of in the next: if (value has :toNumber) or if (value instanceof ConvertibleToNumber) - where if it was working the has is preferrable as it generates less code

  • I didn't miss the point at all, although perhaps I was unclear, since I only posted the lines of code that I thought should be changed or added in your own example. My bad!

    1) I said the type checker is limited in the sense that it's not smart enough to figure out how to narrow types based on has, as you're trying to do

    2) I gave workarounds (two types of casts) to get around that limitation

    3) I said these workarounds don't accomplish the goal of strict type-checking because casts aren't safe in Monkey C

    Perhaps I should've given full code excerpts.

    Based on the code *you* posted:

    function getConfigNumber(key as PropertyKeyType, defaultValue as Number) as Number {
        var value = getConfig(key);
        if (value != null && value has :toNumber) {
            value = (value as Number).toNumber(); // <==== MODIFIED
        }
        if (value instanceof Lang.Boolean) {
            return value ? 1 : 0;
        }
        return value != null ? value as Number : defaultValue;
    }

    Or

    // ADDED
    typedef ConvertibleToNumber as interface {
        function toNumber() as Number;
    };
    
    function getConfigNumber(key as PropertyKeyType, defaultValue as Number) as Number {
        var value = getConfig(key);
        if (value != null && value has :toNumber) {
            value = (value as ConvertibleToNumber).toNumber(); <=== MODIFIED
        }
        if (value instanceof Lang.Boolean) {
            return value ? 1 : 0;
        }
        return value != null ? value as Number : defaultValue;
    }

    Again, I'm not happy with either of these approaches, because as soon as you do a cast in Monkey C, you defeat the purpose of type checking in the first place, since casts are unsafe in Monkey C. (Casts are also unsafe in typescript, but to a much lesser degree.) So once you do a cast, you're relying on the assumption that your cast is correct and won't lead to runtime errors, as opposed to the type-checker enforcing that for you.

    But they accomplish the goal of getting the code to compile.

    Example of your code failing to compile (with strict type checking), as you said:

    Example of my code compiling:

    I didn't try to run it, although I don't have any reason to believe it would fail to run, and my understanding is that this post isn't about that, anyway.

  • OK, this is interesting. Not only did I misunderstand, but our compilers / prettifiers behave differently. FYI:

    I have SDK 4.1.7, prettier Monkey C 2.0.47

    My 1st error is on the 1st if line: 

    if (value != null && value has :toNumber) {

    Config.mc:116,4: Cannot find symbol 'toNumber' on type 'PolyType<Null or $.Toybox.Application.PropertyKeyType or $.Toybox.Lang.Array<$.Toybox.Application.PropertyValueType> or $.Toybox.Lang.Dictionary<$.Toybox.Application.PropertyKeyType,$.Toybox.Application.PropertyValueType> or $.Toybox.WatchUi.BitmapResource>'.

    (I get this with and without the "as Number" added in the next line.

    My current version is this:

    if (value instanceof Lang.Boolean) {
      return value ? 1 : 0;
    }
    // this should cover Float, Double, Number, String, Long, Char, Symbol
    value = value != null && value has :toNumber ? value.toNumber() : null;
    return value != null ? value as Number : defaultValue;

    This doesn't give a warning in the editor, compiles, and passes unit tests I specifically added for this function today.
    But it shouldn't work IMHO :) When I comment out the if instanceof Boolean then I get these 2 errors:
    ERROR: fr255: Config.mc:123,4: Cannot find symbol ':toNumber' on type '$.Toybox.WatchUi.BitmapResource'.
    ERROR: fr255: Config.mc:123,4: Cannot find symbol ':toNumber' on type '$.Toybox.Lang.Boolean'.
    Of those the 2nd one is being taken care of by the if, but what about the BitmapResource? If value indeed can be that type then even with the if that checks for Boolean it still is supposed to fail to compile with the same 1st error IMHO.
    (I admit my test doesn't check for BitmapResource :) 
    Note that I don't need to cast value to Number in the line where I have the has :toNumber (only in the last line I guess maybe because of this bug: https://forums.garmin.com/developer/connect-iq/i/bug-reports/type-checker-fails-to-remove-null-in-some-cases (well maybe a version of it, that trinary operator doesn't remove Null) Though maybe it is because in the last line value still could be BitmapResource?