Struct
new to Analytica 7.0
Struct class
A Struct object defines a new atomic data type. A struct appears on a diagram with a shape that resembles a function shape, but with flat midsections on both the left and right.

After you have introduced a Struct, you can create instances of the struct. Each of those instances is atomic and immutable. The instance is atomic in the sense that Array Abstraction treats it as a single cell. It is immutable in the sense that once an instance is instantiated, it cannot be destructively altered.
A struct instance can have multiple member fields, each of which can have a different data type and dimensionality. For example, a single struct could have a scalar member, a 1-D member and a 2-D member, where the dimensions of the members are unrelated. The indexes of each member do not combine.
Note that a Struct is an Object, an instance of a struct is a Value.
When to use
Many programming languages have ways of bundling values that are similar to Structs, sometimes called Structs, Records or Classes in the respective language. In those languages, these are often the preferred way of organizing information into packets. It should be emphasized that these same motivations do not translate to Analytica in most cases! In general, you should not turn to Structs for simple data organization. In theory you could organize data in that way, but that would not be a best practice. Instead, in most cases in Analytica, you should organize your information around Variables and Indexes. Different types of quantities should be in different global Variables, and the association of two quantities as belonging to the same "instance" is established by using a shared Index.
In Analytica the Struct serves a more specific purpose -- to group things in a way that avoids the automatic combination of indexes by Array Abstraction. References exist for this same purpose, and indeed a Struct could be viewed as a generalization of a Reference. In fact, if Analytica didn't already come with a Reference data type, you could implement it using a Struct.
A Struct can also be used to enforce a distinction that does not exist in Analytica, but might exist in another platform or service that you are integrating with.
If your data type should act as if it is an opaque atomic unit, and especially if there are multiple internal items, then a Struct is a good candidate. A struct can also provide one way to return multiple return values with differing dimensionalities as an alternative to capturing multiple return values.
With that said, there are cases involving complex algorithmic logic for which bundling information within a Struct does make sense, often involving passing complex data structures between many user-defined functions.
Defining a Struct
To define a new data type, you create an object whose class is Struct. You can do this by first creating a user function object, and then in its Object window, changing its class to Struct.
Like a User-Defined Function (UDF), a Struct uses the Parameters attribute with precisely the same Function Parameter Qualifiers as a UDF. One example would be:
Struct MyDtype( a : atom ; b : Text [I] ; c: Number[J] )
This data type has three members -- a, b, and c. The a field can have any atomic data type, the b member must be text and is allowed to have the index I, and the c member must be a numeric and is allowed to have index J.
The default Definition of a Struct is Self. Sometimes it makes sense to include a more specialized definition -- see #Constructor customization below.
Instantiating a value
To create an instance of a Struct (an atomic value), you simply call the name of your struct as if it were a function from within an expression. In the previous example, that might be:
MyDtype( Category, Category_aliases, Category_IDs )
If there are extra dimensions in any of the values passed to the constructor parameters, Array Abstraction will kick in and you'll end up with multiple struct instances, each instance being one cell in the array-values result.
Accessing member values
Given an instance, use the Arrow operator to access member values. E.g.,
inst -> b
where inst was obtained by calling the Struct.
The left-hand side of the arrow can be a single instance, an array of instances, or even Null. The right-hand side of the arrow is the name of a member value.
Member functions
You can introduce your own member functions that are called using the syntax:
inst -> F(x,y)
The parameters are limited to be passed by values (e.g., no Index parameters). Keep in mind that the instance is immutable so these functions are not used in general to modify the internal state of the instance (although see the Section #Immutability below for exceptions).
To create your own member function, open up the Struct's diagram (as you would open up the diagram of a Module node) and drag a Function object to the diagram. In its Object window, set the parameters ensuring that the first parameter is declared as, e.g.,
Function F( Self : MyDtype atom ; param1 ; param2 )
The first parameter should be named Self and should have the Struct name as its data type and the atom qualifier. You can have zero or more additional parameters -- these are the ones passed to the function when it is called using the syntax inst → F(x,y). These parameters can have any data type qualifier, dimensionality declaration; however, at this time they cannot handle evaluation mode qualifiers or Object class qualifiers.
You can also introduce global variables that are "private" to the Struct and its member functions by adding normal variable nodes to the diagram. Note that these are contained in the Struct definition, not in each instance (to have a value that varies with each instance, you must use a member value.
Constructor customization
The member values of an instance are usually the parameter names that appear in the Parameter attribute of the struct (its Constructor's parameter names). However, in some use cases the constructor may change the member values, such as by adding new members, transforming the named members, or even removing members. These changes must be performed in the Definition of the constructor.
When you first view the Object window of a Struct, the Definition might not be visible (because altering the member values is somewhat esoteric). If you don't see it, use the Attribute visibility popup to make it visible.
Inside the constructor (and only in the constructor) the members can be assigned. For example:
Struct MyDType( a : atom ; b : Text [I] ; c : Number [J] ) Definition: Self -> creationTime := Today(true); Self -> c := c / Sum(C, J); Self
The final value is always Self whether or not your Definition actually returns Self, although it is clearer to do. Notice that in this example, it adds a new member value, creationTime, and changes the member value c to the normalized value.
When you change the member values in the constructor, you should consider whether you will ever assign values of your Struct to a definition or other expression attribute, or whether instances will even need to be saved directly in the model file. These cases require a text representation that is valid expression and which results in an equivalent instance. In the event that you alter the members, you may need to add a custom #ExpressionText member function that prints out a constructor call expression.
Using an excess parameter in the constructor
An excess parameter is a catch-all parameter that allows the caller to include named parameters that aren't listed in the parameters. It can be either named or unnamed.
The constructor has an unnamed excess parameter when the last parameter is named "...". For example,
Struct MyDType2( a : atom ; b : Text[I] ; ... )
When declared in this fashion, a caller can specify parameters other than a and b, for example:
MyDType2( a:5, b:f"I={I}", c:14, d:=J^2 )
When an unnamed excess parameter is used in this way, the additional parameters are automatically added as members. Hence, in this example, MyDType2 has members ['a', 'b', 'c', 'd']. Because the excess parameter is unnamed, there is no way to refer to it in the constructor's definition.
To specify that the excess parameters not be added to the struct instance, you can use
Struct MyDType3( a : atom ; b : Text[I] ; ... : nonMember )
A named excess parameter is declared using a named that starts with "...", which again must be the last parameter in the declaration. For example,
Struct MyDType3( a : atom ; b : Text[I] ; ...kw )
Inside the constructor's definition, it can now refer to the local variable kw. The value of kw is itself a struct instance where each argument is a member. For example, when called as
MyDType3( a:5, b:f"{I}", y:43, z:'hello' )
The Definition of MyDType3 could reference kw->y or kw->z when called in this example; however, those would fail with an error when y or z is not specified.
Equality and comparison
Two struct instances are equal (i.e., s1=s2 is true) only when they are of the same Struct, have the same member names and have the same member values. In other words, s1=s2 is based on "deep equality".
Shallow equality tests whether two instances are the exact same instance (as opposed to whether the two instances are equal). The following can be used to test whether two instances are the exact same instance:
s1->_addr = s2->_addr
The special _addr member value returns an integer that is unique to each instance.
Inequality comparisons are also based on the "deep" member values, based on the internal member value ordering.
Basic properties about an instance
You can get a list of the member value names using:
inst -> _members
If you have the name of a member value as text in a variable memberName, you can access its value using:
`->`( inst, memberName )
Note that it wouldn't work to write inst -> memberName because that would look for a member value named "memberName".
You can get a handle to the instance's Struct object using
inst -> _type
You can get the name of the Struct (without a namespace prefix) from TypeOf(inst). You can detect that an atom is a struct instance using
TypeOf( inst, shallow:true ) = "Struct"
Special member functions
You can customize the behavior of your struct instances by adding special member functions (with reserved member function names). You add these as you would your own member functions to your Struct's diagram using the reserved name. Your implementation must match the required parameters for the special function.
Customizing how a value is printed
My default, an instance of your Struct will display in a result table cell or in the Typescript console as, e.g., «YourStructName». You can customize this by adding a member function named DisplayName. E.g.,
Function DisplayName( Self : MyDtype atom )
Definition: f"«{TypeOf(Self)} a={Self->a}»"
Because DisplayName occurs while individual cells are drawing on the screen, errors that occur while it is evaluating are not displayed. If an error occurs, it will silently fall back to the default display name.
When your instance is "printed" to an expression (e.g., as a result of an assignment), you need to ensure that the text is a valid expression that creates a logically equivalent instance. This happens by default unless you alter the members in your constructor (see #Constructor customization above). To control this, add a member function name ExpressionText(Self).
Value expansion (in a result table)
When a struct instance appears in the cell of a result table, it appears as a hyperlink. Double clicking on it expands the value. By default, it expands into the member values. You can change this by adding a member function named Expand(Self).
Your expansion code cannot simply create an array of your member values (assuming they have different dimensionalities) since this would combine their dimensions. Instead, you should wrap each non-atomic value in _HyperlinkedCellvalue. For example:
Function Expand( Self )
Definition: [ 'a' : Self->a,
'b' : _HyperlinkedCellValue( Self->b ),
'c' : _HyperlinkedCellValue( Self->c ) ]
You can also introduce a local index for the expansion.
After it expands, the result table displays a title showing the current "path" to the result view. Ideally this title is also a valid expression that produces the same result. When you customize the expansion, you may also need to customize the expansion title. You can do this by adding a member function
Function TitleForExpand(Self : Tuple atom ; subTitle : text atom ; memberNum : optional positive atom )
The «subTitle» is the title up to the point of this expansion, and «memberNum» exposes which cell number was expanded from the previous level.
Finalizers
A finalizer is a member function that is called when the struct instance value is released for last time.
You can add a customized finalizer, for example, to ensure that a resource gets cleaned up. An example use case would be when your Struct is a temporary file, with the finalizer ensuring that the temporary file is deleted when the instance is released. This is an example where the constructor would pick a unique file name that doesn't already exist, and the finalizer calls FileSystemDelete.
To add a finalizer, add a member function named Finalizer(Self).
Finalizers should have a low probability of an error occurring while the finalizer is evaluating. In the event that an error occurs, it is not possible for Analytica to report the error, so the error will occur silently. If you do not catch it (in a Try), your full finalizer code might not run.
Callable values
You can make an instance of your Struct "callable", such that you can call the instance using e.g.,
inst -> ( x, y )
To make it callable, create a member function named `()` -- make sure to include the backtics as part of the name, and no spaces between the parens. For example:
Function `()`( alpha, pwr ) Definition: (1-alpha) * Self->a + alpha * Sum( b^pwr, I )
If you do this, you can also use a Callable object class to hold your instance (or an array of instances) and then call it using the name of your callable as if it were a normal function (even though the logical function itself is computed), e.g.,
- Callable F1 := MyDType( x,y,z )
And call it:
F1(0.3, 2)
Custom atomic subscripting
Your data type can support the syntax
inst -> [n]
by adding a member function named `[]`. It can have either one or two parameters (after Self). With one parameter, only inst -> [n] is supported (so that inst -> [m..n] would array abstract returning a list). With two parameters, where the second parameter is optional, it can include special handling of inst -> [m..n], for example by returning a new collection instance of the same type.
Function `[]`( Self : Tuple atom ; m,n : optional number atom )
In this example, if inst -> [m] is called, then the «n» parameter is omitted. If inst -> [m..n] is called, both parameters are provided. If inst -> [m..] is called, then «n» is INF. And if inst->[..n] is called, then «m» is omitted.
Operator overloading
At present, only these operators can be overloaded by adding a member function with the given name.
- Function `#`( Self )
Overloading the `->` operator
It is possible to overload the right-arrow operator for a Struct, intercepting attempts to read a member value or to set a member value.
If you only want to intercept attempts to read member values, add the following member function in your struct:
Function `->`( Self : YourStruct atom ; member : Text atom ; defVal : optional named )
By sure to change YourStruct to the actual name of your struct. This will be called when code evaluates
inst -> member
on an instance inst of your struct. Your function can return values for members that don't actually exist, or deny members that might exist, or just intercept the read and then forward it in the usual way.
Exceptions: Member functions with names beginning with an underscore are never intercepted (such as the special inst->_members, inst->_type and inst->_addr virtual members). Also, calls to member local functions or member functions are never intercepted -- this intercepts member value access only.
Your implementation of the overload will often want to access the actual members of the class. It must not use the usual arrow operator for this because that would result in an infinite recursion. Instead, you can use the raw member access function, `->!`, which behaves the same same as `->` except that it cannot be overridden. This function can only be called using a function call syntax, it cannot be used as an infix operator.
Here is a contrived example that makes is look like the file contents is a member value. (It would be better to just add a member function for this, but it makes for an easy example).
Struct TextFile( filename : Text atom ) Function TextFile::`->`( Self : TextFile atom ; member : Text atom ; defVal : optional named ) Definition: If TextLowerCase(member) = "contents" Then ReadTextFile(filename) Else `->!`( Self, member, defVal:defVal )
You can also use an override of `->` to intercept attempts to assign to a member, such as an assignment like
inst -> val := newVal
A struct instance is itself immutable, so such assignments are not generally valid, but using an override you can make it appear that a member is mutable. This could be useful, for example, when using a struct to wrap an object from a different programming language, or in a web service, where members can be assigned. The overload could send the change through an API, keeping a familiar assignment syntax.
To add an overload that can also intercept member assignments (as well as reading member values), the member function should have this signature:
Function `->`( Self : YourStruct atom ; member : Text atom ; defVal : optional named ; rhs : optional named )
Inside the definition, use IsNotSpecified(rhs) to determine whether it is a read or an assignment.
Assignment
Sometimes a button's OnClick expression assigns a result value to a global variable. When you do so, it sets the definition of the global variable to an expression that results in the same value when evaluated. It is also an expression that can be saved with a model file.
In the case of a Struct, this means that the expression must be a call to the Struct's constructor with parameter values that result in a logically equivalent instance.
In the cases where you have customized constructor logic that alters what member values are actually stored, if you want assignment (and saving to a model file) to work successfully, you must also include a ExpressionText member function that returns a textual expression for calling your Struct constructor.
Your struct might not need to support being assigned in this fashion, in which case you don't need to worry about it.
Immutability
The Self members can be altered from within the Struct constructor, but cannot be altered once the fully-constructed instance has been returned by the constructor. From that point on, the members of a struct instance are never changed.
However, a struct can hold a Mutable or a captured local which actually makes it possible for the internal state of an instance to be changed. The mutable or the local's identity never changes, and changes to this type of internal state does not result in any downstream invalidation.
Generic Struct
A built-in struct named _Struct (with an underscore) can be used as a generic Struct that you can instantiate on arbitrary members without having to define your own Struct object. To instantiate, every parameter must be named, but can be whatever name you want, e.g.,
_Struct( date:Today(), name: 'Donald', address: '1600 Pennsylvania Avenue' )
Examples
Examples in this section demonstrate various aspects of a Struct and member functions.
Tuple
A Tuple is a ordered list of items, each item having a different dimensionality. It can be implemented as a Struct.
To create a tuple (after the Tuple Struct is defined), use:
Tuple( x, y, z )
with any number of values. The values passed can have different dimensionalities. The collection is held in a single cell (if you have an array of Tuples), and the dimensionalities of the items are kept separate.
Struct Tuple( items : ... ;
direct : hidden optional named boolean atom )
Definition:
If direct Then (
For xi:= repeated items Do
If Not IsReference(xi) then
Error("When using direct:True to Tuple, all «items» must be references");
Self->items := items
) Else (
Self->items := (For xi := repeated items Do \xi)
)
Function Tuple::Length( Self : Tuple atom ) Definition: Size( Self->items, True )
Function Tuple::Expand ( Self : Tuple atom ) Definition: Local i:=f"[{1..size(Self->items)}]"; For ii:=@i do ( Local xi := #(Self->items[@=ii]); If Size(IndexesOf(xi))>0 then _HyperlinkedCellValue(xi) Else xi )
Function Tuple::`[]`(Self : Tuple atom ; m,n : optional number atom ) Definition: If IsNotSpecified(n) Then #(Self->items[@=m,defVal:Null]) Else ( If IsNotSpecified(m) Then m := 1; If n>Size(Self->items) Then n := Size(Self->items); Tuple( ...Self->items[@=m..n], direct:True ) )
Function Tuple::TitleForExpand(Self : Tuple atom ; subTitle : text atom ; memberNum : optional positive atom ) Definition: If IsNotSpecified(memberNum) Then f"{subTitle}->" Else f"{subTitle}->[{memberNum:I}]"
Function Tuple::ExpressionText( Self : Tuple atom )
Definition:
Local itemTxt :=
Local itemRef[] := Self->items Do
ConsolePrint("",#itemRef,silent:true);
f"Tuple({JoinText(itemTxt,,',')})"
Reference
This example demonstrates how a reference could be implemented using Struct if it weren't already built into the Analytica language. Note that the keywords Ref and Reference are already in use, so we need to pick a different name.
Struct _Ref( r : [I] ; I : optional ... index = common )
Function _Ref::`#`( Self : _Ref atom ) Definition: Self->r
Function _Ref::Expand( Self : _Ref atom ) Definition: `#`(Self)
Function _Ref::TitleForExpand(Self : _Ref atom ; subTitle : text atom ; memberNum : optional positive atom )
Definition:
f"#{subTitle}"
Function _Ref::ExpressionText( Self : _Ref atom )
Definition:
f"_Ref({ConsolePrint("",Self->r,silent:true)})"
Enable comment auto-refresher