Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
calculate_functors [2026/07/23 03:55]
hermann
calculate_functors [2026/07/29 13:42] (current)
hermann
Line 172: Line 172:
  
 Unless a restriction is noted, every operator is available in all five calculator functors. Unless a restriction is noted, every operator is available in all five calculator functors.
 +
 +Every construct in the language — including a conditional — is itself just an expression that resolves to one value, and any expression can appear anywhere another one is expected. This makes conditionals freely composable: two ''​if...then...else''​ results can be summed directly, a conditional'​s own test can be replaced with another conditional,​ and so on, without any special syntax for combining them. For example, ''​if (if i1 > 0 then 1 else i2) then i2 else 1 + i3''​ nests one conditional inside another'​s test.
  
 ===== Identifier Syntax ===== ===== Identifier Syntax =====
Line 468: Line 470:
  
 The poison spreads through the entire chain: if ''​i1''​ is null, then ''​i1 + 5''​ is null, ''​(i1 + 5) * v1''​ is null, and so on. There is no way to "​un-null"​ a result once a null has entered the calculation — except by explicitly testing for it first. The poison spreads through the entire chain: if ''​i1''​ is null, then ''​i1 + 5''​ is null, ''​(i1 + 5) * v1''​ is null, and so on. There is no way to "​un-null"​ a result once a null has entered the calculation — except by explicitly testing for it first.
 +
 +A null value inside an ''​if...then...else''​ test propagates the same way, regardless of which branch would otherwise have been taken. In ''​(if (i1 / 3 > 9) then i2 else 1 + i3) + 10'',​ a null ''​i1''​ makes ''​i1 / 3''​ null, which makes the comparison ''​i1 / 3 > 9''​ null, which makes the whole conditional null — and adding 10 to that null result is still null. Wrapping the expression in ''?''​ catches it: ''​( (if (i1 / 3 > 9) then i2 else 1 + i3) + 10 ) ? 20''​ evaluates to 20 whenever the inner expression comes out null for any reason, including a null test clause.
  
 In a realistic expression, null can enter from several independent sources simultaneously. This expression computes a weighted transition probability from two input maps and a per-class calibration table: In a realistic expression, null can enter from several independent sources simultaneously. This expression computes a weighted transition probability from two input maps and a per-class calibration table:
Line 497: Line 501:
  
 > **Note:** ''​isNull()''​ is immune to the poisoning rule — it always returns 0 or 1, never null, even when its argument is null. It is the only reliable way to branch on null. > **Note:** ''​isNull()''​ is immune to the poisoning rule — it always returns 0 or 1, never null, even when its argument is null. It is the only reliable way to branch on null.
 +
 +This poisoning behaviour is what makes short expressions like ''​i1 + 10''​ or ''​i1 * i2''​ work correctly across a whole map without any explicit null-checking:​ a cell that's null in either input becomes null in the output automatically,​ so the expression effectively only touches cells where every input is valid.
 +
 +**Substituting a default for a null operand.** To treat null cells in either of two maps as if they held the value 1 before multiplying them — rather than letting either map's null poison the whole product — the ''?''​ fallback is the most concise form:
 +
 +<​code>​
 +(i1 ? 1) * (i2 ? 1)
 +</​code>​
 +
 +The equivalent using ''​isNull''​ is more verbose but reads the same way:
 +
 +<​code>​
 +(if isNull(i1) then 1 else i1) * (if isNull(i2) then 1 else i2)
 +</​code>​
 +
 +Both are shorthand for the fully exhaustive case analysis, spelled out explicitly:
 +
 +<​code>​
 +if isNull(i1) and isNull(i2) then 1
 +else if not isNull(i1) and not isNull(i2) then i1 * i2
 +else if not isNull(i1) and isNull(i2) then i1
 +else i2
 +</​code>​
 +
 +The exhaustive form is only worth writing out when each of the four combinations genuinely needs different treatment; when they don't, as here, the shorter forms above are equivalent and preferable.
  
 If the computed real value exceeds the range of the chosen Cell Type, that cell is also written as null rather than wrapping or clipping. This range check applies to [[Calculate Map]] and [[Calculate Categorical Map]] only — the only two functors that accept a Cell Type parameter and write to a typed cell grid. If the computed real value exceeds the range of the chosen Cell Type, that cell is also written as null rather than wrapping or clipping. This range check applies to [[Calculate Map]] and [[Calculate Categorical Map]] only — the only two functors that accept a Cell Type parameter and write to a typed cell grid.
 +
 +===== When a conditional'​s test needs guarding against null =====
 +
 +A conditional'​s test poisoning the whole result is only a problem when the tested operand'​s nullness shouldn'​t determine whether the entire expression is invalid. Reclassifying ''​i1''​ in place — ''​if i1 = 2 then 10 else i1''​ — needs no explicit guard, since a null ''​i1''​ making the reclassified result null for that cell is exactly the desired behaviour: the expression is internally equivalent to ''​if isNull(i1) then null else if i1 = 2 then 10 else i1''​.
 +
 +The same mechanism causes a real bug when the operand being tested isn't the one the null-ness should actually be judged against. Combining a landscape map (''​i1''​) with a road map (''​i2'',​ where 1 marks a road and null marks everything else) might suggest ''​if i2 = 1 then 1 else i1''​ — but this is internally equivalent to ''​if isNull(i2) then null else if i2 = 1 then 1 else i1''​. Since most cells are null in a sparse road map, most of the output becomes null too, discarding ''​i1'''​s landscape values everywhere a road isn't present — even though ''​i2''​ being null says nothing about whether ''​i1'''​s value is valid.
 +
 +Guarding the test with ''​isNull''​ fixes it: ''​if not isNull(i2) and i2 = 1 then 1 else i1''​ only tests ''​i2''​ once it's confirmed non-null, so a null road cell falls through to the landscape value in ''​i1''​ instead of nulling the result.
  
 ===== Exceptions in map calculators ===== ===== Exceptions in map calculators =====