Expressions that don't array-abstract


Principle of Array Abstraction

A key source of the convenience and power of Analytica is array abstraction, embodied in the feature called Intelligent Arrays. The general principle of array abstraction can be expressed as follows. Suppose f(x) denotes any function, expression, or sub-model that computes a result from input(s) «x», and which does not operate over the index I. Then for any input «x» (which may be array-valued, and may contain the index I), and any element n of I, the following holds:

f(x[I = n]) = f(x)[I = n]

In English this says that if you compute your model on the nth slice of the input, you'll get the same thing you'd get if you compute your model on the entire array input and then keep just the nth slice of the result.

This simple concept turns out to be extremely powerful. Notice that the concept applies at every level -- at the level of individual function calls or individual operations (like +, *, etc), all the way up to the level of your entire model. The uniformity of array abstraction allows you to add and remove dimensions from our models easily, even after the logic is fully encoded, at which point you are better able to understand your problem and decide which subdivisions lead to the greatest insight. It also allows us to think abstractly about our problem, rather than worrying about the mechanics of computation, greatly simplifying cognitive load while building models.

Expert modelers learn the importance of ensuring that models fully array-abstract, so that you can add new dimensions at any time and feel confident that the correct results continue to be computed.

Note that if f(x) operates over the index I, then the above relationship does not necessarily hold. For example, expressions Max(A, I) or Cumulate(A, I) operate over the index «I». The principle only holds for indexes that the function or expression does not operate over.

Limitations

The vast majority of Analytica functions and operators obey the above principle of array-abstraction. It is helpful to be aware of those few that don't, so that you can treat them accordingly. In most cases, you can rewrite expressions so that they will array abstract. Functions and operators that don't array abstract fall into these categories:

  • Functions that create lists, such as Sequence, Subset and Copyindex: These cannot accept parameters that are arrays (or have more than one index) because they must generate lists which can have only one dimension.
  • While loops, because the condition c in WHILE c DO e must be a scalar Boolean -- True (1) or False (0).
  • Assignments in conditional statements like If-Then-Else.
  • Functions that simply violate the array-abstraction principle. E.g. Size(A), which returns the number of elements in Array «A».
  • Expressions that assume scalar or 1-D parameters. These usually arise from omitting indexes unnecessarily -- by specifying the index you are operating over, your expressions can usually be made to array abstract.

There are rare cases where it is appropriate to use expressions that do not array-abstract. Even in these cases, you can often ensure that the overall definition array abstracts by, surrounding declaration qualifiers. Nevertheless, a key skill for becoming an expert Analytica modeler is learning to recognize these cases, and understanding how to reformulate expressions so that they do array abstract.

List-Generation Functions

Consider the function: Sequence(a, b), or equivalently a..b. For example:

Sequence(1, 5) → [1, 2, 3, 4, 5]
Sequence(3, 8) → [3, 4, 5, 6, 7, 8]
1 .. 5 → [1, 2, 3, 4, 5]

Suppose that b := [5, 6, 7]; then Sequence(1, b) might generate three sequences:

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 7]

If you try to combine these along the index «b», the result would be a non-rectangular array. Analytica disallows this, since it does not support non-rectangular arrays. Hence, Sequence does not array-abstract. Its parameters must all be scalar i.e. single numbers, not arrays of numbers. If you add a dimension that propagates to a parameter of Sequence, it will cause an error message when Sequence is evaluated.

This limitation applies to all functions that evaluate to a list, including:

When you need to use a function that returns a list result, and you need it to operate in an array-abstractable fashion, one method is to pad the shorter results with Null values. The functions Subset, SplitText and Concat provide an optional index parameter for the result, which does exactly this, thus saving you the work of re-indexing the intermediate results onto a known index. For list-generating functions that do not do this, you can accomplish this with your own code, often involving explicit For..Do loops.

Expressions assuming 1-D parameters

For some array functions, including Sum(A, I), Max(A, I), and Min(A, I), the second Index parameter «I» is optional. If omitted, the function selects the outermost index of «A». If «A» has more than one index, it's not obvious which index this is, and it may change if the dimensions of «A» change, and so the result may be ambiguous and not what you intend. For this reason, you should always include the index explicitly as a parameter (except in those very rare cases where you intend to operate over an implicit index).

While loop conditions

A While loop While cond Do expr

evaluates «expr» repeatedly until «cond» becomes true. While requires «cond» to be scalar, so that the termination condition is non-ambiguous. Adding a dimension that propagates to «cond» may result in an error being reported by While that «cond» is not scalar.

Assignment inside If-Then-Else

The conditional construct If A Then B Else C in Analytica is evaluated in an array-fashion. This can lead to some non-intuitive behaviors, and in particular to expressions that violate the law of array-abstraction if you do anything with side-effects (e.g., assignment) inside the Then or Else clauses.

The following example demonstrates (for more information, see the "Gotchas" section at Assignment operator:

Var b := [true, false];
Var r := 0;
If b then r := r + 1 else r := r - 1;
r
→ 0

When this is evaluated, since b contains both true and false values, both the then and else clauses are evaluated. r is incremented then decremented, so the final result is 0. But:

Var b := true;
Var r := 0;
If b then r := r + 1 else r := r - 1;
r
→ 1
Var b := false;
Var r := 0;
If b then r := r + 1 else r := r - 1;
r
→ -1

The right way to do this is to assign to r the result of the conditional expression:

Var b := [true, false];
Var r := 0;
r := If b then r + 1 else r - 1
→ [1, -1]

Functions that violate the Array-Abstraction Principle

Some functions simply violate the law of array-abstraction by the very nature of what they do. Many meta-inferential algorithms fall into this category. Perhaps the most common yet subtle is the Size function. As an exercise, dhow that Size(A) violates the law of array-abstraction expressed at the top of this page.

Obtaining Abstractable Expressions via Encapsulation

Suppose you have an expression that works in the simple case, but does not array-abstract for one of the above reasons. Although you may be able to find a way to write it differently, you may also be able to encapsulate it appropriately so that the full expression does array abstract.

Consider the example above demonstrating the perils of using side-effects inside an If-Then-Else. In this case, we have a single variable, b, serving as input to the algorithm. By augmenting the declaration of b</code. to indicate the dimensionality that <code>b should have inside the expression, Analytica can iterate appropriate so that b will be atomic when the If condition is evaluated. The following two declarations are equivalent syntaxes for declaring that b is atomic (i.e., zero-dimensional):

Var b[ ] := ...

or

Atomic b := ...

The first syntax lists the allowed dimensions of b between brackets -- in this case, an empty list, indicating that no dimensions of b are allowed.

Var b[ ] := [true, false];
Var r := 0;
If b then r:=r + 1 else r:=r - 1;
r

→ [1, -1]

A second method for encapsulating, which is more efficient when there are two or more inputs, is to wrap the expression in a User-Defined Function and declare the dimensionality of each parameter:

Function F(b : atom) :=
Var r := 0;
If b Then r := r + 1 Else r := r - 1;
r
F([true, false]) → [1, -1]

When called with F([true, false]), Analytica uses the dimensionality information to iterate so at to obtain a result consistent with the principle of array-abstraction.

See Also

Comments


You are not allowed to post comments.