Strict typing problem

I'm trying to get my app strict typing compliant. Already hit the wall with a simple array:

using Toybox.Lang;
...
var showList = [0, 0, 0] as Lang.Array<Lang.Number>;
...
showList[0] = 4;
produces
Cannot determine if container assignment is using container type.
What's wrong here?
  • The data flow is as follows:

    Step 1: Data is coming in from a sensor as type String, format similar to NMEA sentences as known from gps devices
    e.g."$FL5, 240, 0C00, 223, 675662, 2"

    I parse the input string, break it down in terms and store the terms as strings in an Array of String:

    _FLterm
    as Array<String> = new [15] as Array<String>

    Step 2: After String parsing is complete, the terms are converted to values and arranged in an array of number:

    FLdata as Array<Number> = new [FL_tablesize] as Array<Number>;
    FLdata[FL_temperature] = _FLterm[1].toNumber() as Number;

    Step 3: To display the values, i do some conversions and put the result in a displayString for the simpledatafield:
    _displayString += (_data.FLdata[FL_temperature] as Number / 10.0).format("%.1f").toString() + "°C";
    The given example should result to "24,0°C". 
  • add:
    System.println("FL_temperature:" + FL_temperature);
    System.println("_data.FLdata[FL_temperature]:" + _data.FLdata[FL_temperature]);
    before the line where you get the null exception

  • _displayString += (_data.FLdata[FL_temperature] as Number / 10.0).format("%.1f").toString() + "°C";

    Couple of things here, not directly related to the problem.

    You shouldn't explicitly cast to Number ("as Number") because that removes type safety, as mentioned before.

    ".toString()" is unnecessary here as Float.format() already returns a string.

    FLdata[FL_temperature] = _FLterm[1].toNumber() as Number;

    Is this the only place where items are assigned to the array? Here, "as Number" is redundant. Again I would avoid explicit casts except where required by the compiler.

    FLdata as Array<Number> = new [FL_tablesize] as Array<Number>;

    Seems to me that this is the problem.

    This actually defines an array of size FL_tablesize that is initialized with nulls. Say FL_tablesize is 5. Then FLdata will initially look like this:

    [null, null, null, null, null]

    Here's where the explicit cast to Array<Number> is an issue. Again, this is also a compiler issue IMO, since it allows you to do the cast in the first place. Since it's initialized with nulls, FLdata should actually be Array<Number or Null>, not Array<Number>, but with the power of explicit casts, you were able to write code that lies to the compiler about the type of an object.

    Obviously the problem at runtime is that FL_temperature is referring to an array element whose value hasn't been reassigned yet. That could be a bug in and of itself (i.e. FLdata[FL_temperature] should never be null), or it could be an expected condition that isn't handled properly. Without knowing the full picture, I can't say either way.

    As mentioned by myself and Gavriel, you can validate that by using System.printlns before the code that crashes.

    I would change the code as follows:

    var FLdata as Array<Number or Null> = new [FL_tablesize] as Array<Number or Null>;
    //...
    var val = _data.FLdata[FL_temperature];
    if (val != null) {
      _displayString += (val / 10.0).format("%.1f") + "°C";
      /* the compiler is smart enough to infer that val is a Number (not Number or Null), at least for the first line after "if (val != null)" */
    } else {
      // do whatever you need to do
    }

    Note how I've written the code specifically to avoid explicit casts with "as", because as noted, explicit casts actually remove compile-time type safety.

  • You hit the nail: I forgot to initialize the FLdata[] Array with 0's. Added this, and now it runs.

    Now i am facing the next type problem with the conversion from my String Array:

    FLdata[FL_temperature] = _FLterm[3].toNumber();
    works, but
    FLdata[FL_temperature] = _FLterm[3].toNumber() * 10;
    gives compiler error
    Attempting to perform operation 'mul' with invalid types 'Null' and '$.Toybox.Lang.Number'.
    _FLterm[] is initialized with "0" 's.
     
  • That's because _FLterm[] is an array of strings (both the declared type and, I assume, the actual contents) and String.toNumber() can return null. (The compiler has no way of knowing whether your string is a valid representation of a number at compile-time.)

    toNumber() as Lang.Number or Null

    Convert a String to a Number.

    If a String is in the numeric form of "123", it can be converted to a Number. Additional characters after the detected number value will be ignored. Strings that cannot be interpreted as a Number, or whose value exceeds that which can be represented in a Number, will result in a null value.

    You need to either explicitly do a cast (which is dangerous) or add logic to handle the null case. You can do the former if you're 100% sure that every String element of _FLterm[] will always be convertible to a valid Number, but if I think if you have the memory, the null check is better.

  • Understood, but why does the compiler not complain this issue without the mul operation?

  • That's a good question, maybe you should open a bug report on that. toNumber returns Number or Null, so I agree with you that it should be a compile time error when in strict mode

  • Understood, but why does the compiler not complain this issue without the mul operation?

    I don't know. Does your current code have FLData as Array<Number> or Array<Number or Null>?

    If it's the latter, then it makes sense why the compiler wouldn't complain. If It's the former then I would say that's a bug.

    In my testing, seems like it's yet another bug, as Gavriel said.

    // member variables
    var arrayWithNulls as Array<Number or Null> = new [1] as Array<Number or Null>;
    var arrayWithoutNulls as Array<Number> = [0] as Array<Number>;

    // test code
    arrayWithNulls[0] = 5; // ok
    arrayWithNulls[0] = "foo".toNumber(); // ok
    arrayWithNulls[0] = null; // ok

    arrayWithoutNulls[0] = 5; //ok
    arrayWithoutNulls[0] = "foo".toNumber(); // ok (seems like a bug)
    var x = "foo".toNumber();
    arrayWithoutNulls[0] = x; // ok (seems like a bug)
    arrayWithoutNulls[0] = null; // compile error, as expected (*)

    (* Cannot assign '[$.Toybox.Lang.Number] = Null' to '$.Toybox.Lang.Array<$.Toybox.Lang.Number>'.)

    The compiler definitely knows that "foo".toNumber() is Null or Number.

    var nullMember as Null = null;

    nullMember = "foo".toNumber(); //compile error, as expected (**)
    (** Assigned value type 'PolyType<Null or $.Toybox.Lang.Number>'to member '$.testdf3View.nullMember' of non-poly type 'Null'.)
  • It's Array<Number>, not Array<Number or Null>. So the compiler could (should?) know.

  • It's Array<Number>, not Array<Number or Null>. So the compiler could (should?) know.

    Agreed. As posted above, I was able to recreate your problem.