Number vs Float discrepancy between simulator and device

I’ve run into a weird issue and would like your opinions.

I have code that performs some calculations that should produce a Number, and then instantiates an object whose constructor expects a Number.

The compiler seems happy that these calculations result in a Number and allows me to pass the result into the constructor. Later, I store that value in a member typed as Object, and later convert it back to a Number again. At that point I had a safety instanceof check to confirm the Object really is a Number.

In the simulator everything works fine, but on some real devices (under some conditions) it looks like the calculation results in a Float. Monkey C “duck typing” then lets the Float propagate without complaint until my explicit check, which is how I noticed.

To me the core issue is that the compiler believes the code results in a Number, the simulator behaves accordingly, but real devices sometimes behave differently.

Here is the relevant code (with comments):

var minValue = sitemapNumeric.getMinValue(); // returns a Number
var maxValue = sitemapNumeric.getMaxValue(); // returns a Number
var step     = sitemapNumeric.getStep();     // returns a Number

var currentValue =
    numericItem.hasState()
    ? numericItem.getNumericState() // returns a Number
    : (minValue + Math.round((maxValue - minValue) / (2 * step)) * step).toNumber();

for (var i = minValue; i <= maxValue; i += step) {
    if (_currentIndex == -1 && currentValue <= i) {
        _currentIndex = _pickables.size();
        if (i != currentValue) {
            _nonConforming = new NumericPickable(currentValue, unit); // expects Number
            _pickables.add(_nonConforming);
        }
    }
    _pickables.add(new NumericPickable(i, unit)); // expects Number
}

And here is NumericPickable:

class NumericPickable extends CustomPickable {
    public function initialize(value as Number, unit as String) {

        // Workaround: force it to be a Number, so later reads are consistent
        value = value.toNumber();

        // CustomPickable stores the first parameter as Object
        CustomPickable.initialize(value, value.toString() + unit);
    }
}

Has anyone seen something like this before?

Once I understand what’s going on, the workaround is easy (the extra toNumber() in NumericPickable). But I’d really like to understand why the compiler and simulator treat this as a Number, while some real devices end up producing a Float.

If anyone is interested, here are the full source files. The first file contains the calculation discussed above, starting at line 39:

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumbericPickerFactory.mc

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumericPickable.mc

  • If you actually want non-integer division to be performed, you have to ensure one of the operands is a non-integer (e.g. multiply by 1.0).

    Interesting, another thing learned. I just checked, and even if I remove Math.round() and toNumber(), the code still compiles.

  • Btw, if you can reliably demonstrate the discrepancy between the sim and a real device, I think it's worth filing a bug report. 

  • If you actually want non-integer division to be performed, you have to ensure one of the operands is a non-integer (e.g. multiply by 1.0).

    Interesting, another thing learned. I just checked, and even if I remove Math.round() and toNumber(), the code still compiles.

    Sure, C, Java and C# work the same way, but not js or python. (So I can def understand why it would be a surprise.)

    Monkey C clearly copies a few things from Java.

    [https://developer.garmin.com/connect-iq/monkey-c/]

    As Italian and Spanish derive from Latin, Monkey C derives heavily from other popular languages. C, Java, JavaScript, Python, Lua, Ruby, and PHP all influenced the design for Monkey C. If you are familiar with any of those languages, Monkey C should be easy to pick up.

    Too bad those languages are very different from each other....I feel like some of the designers for those languages would be horrified if you told them you were gonna mash all of them up.

  • "If you are familiar with any of those languages, Monkey C should be easy to pick up."

    These is supremely ironic, as nobody who knows even a handful of those languages would think something like "oh if you told me there's this new language which is a mashup of Java, Python and PHP, it'll be super intuitive to learn!!!!!111!!" 

    No, actually to me it seems that if you mash up all those languages with competing and contradictory design philosophies, goals, and implementations, it's actually less intuitive than picking up any one of those languages.

    It's like saying "we're gonna create a sport that combines hockey, basketball, american football and soccer (football)! should be super intuitive for anyone who knows any of those sports!"

  • There's an even "better" way:

    var value = _jsonObject[0];
    if (value != null && value :has :toNumber) {
    return value.toNumber();
    }
    return def;

    This will even work if the json has:
    {"pi":"3.14"}
    if the string is numeric. Although you might need other checks or try/catch if the string could be non-numeric.

    Also have you ever really tried: x instanceof Numeric?
    Does it really work? Because Numeric is not a type, it's a typedef:

    Numeric as Lang.Number or Lang.Float or Lang.Long or Lang.Double
    and IMHO the only common type is Object in this case.

    And yes, I was right:

    var a = 2.3;
    if (a instanceof Numeric) {
    log("Numeric");
    } else {
    log("oops");
    }

    This doesn't even compile:
    Undefined symbol ':Numeric' detected.

  • If you ever create your own language, make it typed from the start. 

    Don’t be clever and make it untyped. 

    Because you will figure out that was a mistake and just makes more work for people later to fix it. Like people before you.

  • If you ever create your own language, make it typed from the start. 

    Don’t be clever and make it untyped. 

    Because you will figure out that was mistake and give people work later changing it. Like people before you.

    Yeah it's funny how js, python, and Monkey C all went through the same thing - they started out without strong types and with only duck typing (for ease of use), then they ended up adding type checking as a distinct compile-time layer.

    To be fair, Connect IQ and Monkey C were first released in 2014, only a couple of years after TypeScript was initially released in 2012.

    But then again, Monkey Types wasn't added to Monkey C until 2021.

    Yeah I get that Garmin has less resources to devote to CIQ than Microsoft (typescript), the python maintainers, etc.

  • This will even work if the json has:
    {"pi":"3.14"}
    if the string is numeric. Although you might need other checks or try/catch if the string could be non-numeric.

    Yeah I thought of that but I had the same objection that you wrote yourself, which was my exact reason for not suggesting it.

    I suppose if toNumber() fails, the default value could still be used though.

    I think it makes the code more complex while introducing behaviour that was not necessarily needed nor desired. But it's def an option if there's a requirement to accept numerical strings as input for this function which takes a JSON field and returns a Number.

    Also have you ever really tried: x instanceof Numeric?
    Does it really work? Because Numeric is not a type, it's a typedef:

    I could've sworn I tried it successfully in the past, but I guess I was wrong (either that or something changed).

    I agree with your reasoning 100% tho.

    It could obviously be rewritten equivalently as:

    if (value instanceof Number || value instanceof Long || value instanceof Float || value instanceof Double) {

  • To be fair, Connect IQ and Monkey C were first released in 2014, only a couple of years after TypeScript was initially released in 2012.

    To be fair, the Monkey C developers should have known better. 

    TypeScript had the issue of “modernizing” a well-established language created in 1995. Python was created in 1991. PHP was released in 1993 with (complete) types in 2015. Monkey C is C-like, which had types in the 70’s.

    It’s like the Monkey C creators had no idea there were lessons from many other languages.

  • To be fair, the Monkey C developers should have known better. 

    TypeScript had the issue of “modernizing” a well-established language created in 1995. Python was created in 1991. PHP was released in 1993 with (complete) types in 2015. Monkey C is C-like, which had types in the 70’s.

    It’s like the Monkey C creators had no idea there were lessons from many other languages.

    Point well taken. I do remember that even when Connect IQ was initially released almost 12 years ago, interested developers on reddit complained that Monkey C lacked modern features which were in vogue then (and still are), such as string interpolation and functional programming.

    In general I don't think Garmin is the best at learning from history or its own mistakes. I see the same bugs recur over and over again, and I've seen a lot of design decisions in CIQ that seem ill thought out. At least one design issue was resolved years later, when CIQ added the ability to scope resources to app settings.

    Then again, idk if I'm the best person to judge, as I'm not sure I could do a better job. It's easy for me to sit back and complain (as I love to do haha).

    I did create a kind of excel-like language for my AppBuilder CIQ data field (I like to call it "excel for your watch" man facepalming), and there's a few things I disliked about it from the start, as well as a couple of design decisions I regretted (one of which I did reverse in a non-backwards compatible re-release of the app). 

    Then again, that app's active user count is prob in the dozens (maybe hundreds, to be generous), so at least the impact is minimal haha.