Differences
This shows you the differences between two versions of the page.
| 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 ===== | ||