Best Practice for Loading Strings, Fonts, and Settings in Watch Face with Background and Typecheck

I have a watch face with a background process. In the View class, I have several class-level constants for loading custom fonts and strings (to use in the foreground). I also have a couple of color variables. The type checker doesn't like any of these because they are "not available in all function scopes" (i.e., background). But because they're at the class level, I cannot bypass type checking with (:typecheck(disableBackgroundCheck) like I could with a function.

Is there any way I can use class-level constants and variables like this in an app with a background process and avoid type checking issues?

class MyWatchFaceView extends Ui.WatchFace {

	var foregroundColor as Gfx.ColorType = Gfx.COLOR_WHITE;
	var backgroundColor as Gfx.ColorType = Gfx.COLOR_BLACK;  
	
	const fontSmall = Ui.loadResource(Rez.Fonts.small);
	const fontLarge = Ui.loadResource(Rez.Fonts.large);

	const weekdays = [
		Ui.loadResource(Rez.Strings.Day0),
		Ui.loadResource(Rez.Strings.Day1),
		Ui.loadResource(Rez.Strings.Day2),
		Ui.loadResource(Rez.Strings.Day3),
		Ui.loadResource(Rez.Strings.Day4),
		Ui.loadResource(Rez.Strings.Day5),
		Ui.loadResource(Rez.Strings.Day6)
	];

	const months = [
		Ui.loadResource(Rez.Strings.Month00),
		Ui.loadResource(Rez.Strings.Month01),
		Ui.loadResource(Rez.Strings.Month02),
		Ui.loadResource(Rez.Strings.Month03),
		Ui.loadResource(Rez.Strings.Month04),
		Ui.loadResource(Rez.Strings.Month05),
		Ui.loadResource(Rez.Strings.Month06),
		Ui.loadResource(Rez.Strings.Month07),
		Ui.loadResource(Rez.Strings.Month08),
		Ui.loadResource(Rez.Strings.Month09),
		Ui.loadResource(Rez.Strings.Month10),
		Ui.loadResource(Rez.Strings.Month11)
	];

    (:typecheck(disableBackgroundCheck))
	function initialize() {
        WatchFace.initialize();
   	}
   	
   	...
}

For a separate but related question, in the App file, I have a global variable for a custom class for app settings (e.g., "gSettings"). If I try to initialize it at the global level by creating a new class instance, I get an illegal error in the debug console. So for now I'm living with the "untyped" warning.

Is it possible to have a global variable assigned to a custom class and avoid type checking issues?

  • The type checker pretty much tells you what to do. It's never to change your logic. It's always to change the declaration or in certain cases you might need an explicit type cast (most of these cases are due to it being not smart enough, so I consider it a type checker bug)

    So in 95% of the cases the only thing you need to do is to add either at the declaration or in some expression "as Type"

    If you change your logic because of type check warnings, then you probably should rather turn it off - though as a new Monkey C developer, I think you could benefit from having it enabled, but that needs you to go WITH it and not to try to WORK AROUND it.

  • It's never to change your logic

    I disagree. For example, sometimes the type checker will tell you that you are accessing a field that’s typed to be T or null, but ignoring the fact that it might be null. If such a field is an API field, then you literally can’t change the type declaration, and you probably shouldn’t cast the value. [I’m not referring to the kind of situation where loadResource returns a Resource, but you know for sure that you can cast it to FontResource bc you specified a font resource id as an argument]

    Sometimes the correct solution to a type checker error is to change your logic to handle the case where a field is a type (such as Null) that you weren’t expecting.

    [1/x]

  • e.g. If you build the following code on SDK 7.1.0 with type check level 2 or higher, you will get a warning or error about calling substring on observationLocationName, because observationLocationName is typed to be String or Null, and substring is a method on String [obviously the Null type has no methods].

    [Yes, I purposely chose an older SDK because observationLocationName is now deprecated, so you actually shouldn’t be using it at all. But since the failure to check observationLocationName for a null value is a real problem that affects real apps and it could have been caught with type checking, I’m using it as an example.]

    var currentConditions = Weather.getCurrentConditions();
    if (currentConditions != null) {
      var abbreviatedLocationName = currentConditions.observationLocationName.substring(0, 10); // should produce error to the effect of “symbol ‘substring’ not found on Null” or something like that

      // yes, in this case it doesn’t “look” like a type check error, but it’s actually the type checker that detects whether a symbol exists on a given type
      // I could’ve contrived a different example which would’ve produced a type mismatch error to the effect that you can’t pass String or Null into a function that expects only string
      // …
      // draw locationName on screen
    }

    [2/x]

  • Ofc a null check (as in the example above) is just one specific example of a type guard.

    And again, I would argue that a type cast should always be the option of last resort, only to be used when:

    - the API gives you no choice (e.g. when calling Application.Storage.setValue() on the data returned by makeImageRequest on a CIQ 4+ device). This is usually due to a bug or limitation in the API / type system.

    - you are *absolutely* sure you know what you are doing. (The problem is that, unlike TypeScript, Monkey C will make no attempt to verify whether your type cast makes sense. TypeScript will warn you if you try to cast between unrelated types, meaning that it tries to prevent unsafe casts. Monkey C has no such protection against unsafe casts)

  • Ok, so this is an example where the type checker found an actual bug in the code, and fixing it indeed needs changing the logic (need to check if observationLocationName is null), but that's the way the code should've been written even without type checker.

    But when you declare a variable as: var foo; and the type checker reminds you to state it's type, and you change the code to: var foo as Number; then this is not changing the logic.

  • so this is an example where the type checker found an actual bug in the code

    Yes, the purpose of the type checker is literally to find type errors. Type errors often point to actual bugs in the code. Long before the existence of Monkey Types, Monkey C programs have crashed due to run-time type errors. (e.g. an API function expected a Number argument but a String was passed in).

    The type checker wasn’t introduced to Monkey C just to torture devs by forcing them to participate in Monkey Types kabuki theatre through adding meaningless type declarations and casts to their code.

    But when you declare a variable as: var foo; and the type checker reminds you to state it's type, and you change the code to: var foo as Number; then this is not changing the logic.

    I agree, but that’s only one kind of problem that the type checker can point to.

    The statement I specifically disagreed with was:

    The type checker pretty much tells you what to do. It's never to change your logic” (emphasis mine)

    Going back to this statement: “so this is an example where the type checker found an actual bug in the code”

    Not to state the obvious, but even though a missing type declaration for variable foo is not a bug per se, it does indicate that the type checker cannot definitively identify type issues related to foo which could be associated with real bugs.

    For example, I declare a non-local variable foo and I leave its type unspecified. At various points in the program, I assign either a number or string to foo. At other points in the program, I pass foo into a function bar() as the 1st argument, which is typed as Number. bar() performs division with the 1st argument as the numerator and another number as the denominator. Obviously the division will crash if the argument is actually a string.

    Clearly there is a bug here somewhere, but where? The type checker can’t know whether the bug is that foo shouldn’t have been passed into bar (because the dev’s intent is for foo to be either a number or string), or whether the bug is that foo shouldn’t have been assigned a string value (which means it’s ok to pass foo into bar).

    By asking me to declare the type of foo (at type check level 2 or 3), the type checker can resolve this ambiguity.

    If you type foo as Number (only), then the assignment of a string to foo is the bug.

    If you type foo as Number or String, then passing foo into bar is the bug.

    TL;DR the purpose of the type checker is prevent type-related bugs. Declaring types for all non-local variables, function arguments and function return values helps it do its job. Given that a variable’s type is declared (which is the case for all API fields), you may also have to change your logic to use that value properly.

    So one could reasonably say there are two classes of warnings and errors emitted by the type checker:

    - warnings/errors that the type checker doesn’t have enough information to do its job properly (this is the kind of thing I think you were mostly talking about)

    - warnings/errors that point to actual bugs in the code. (Yes, to fix these, sometimes a type declaration needs to be changed or maybe a type cast needs to be added, but other times, logic needs to change.)