Namespaces


New to Analytica 6.5, and still experimental, not fully exposed, unsupported and subject to change.

This page describes a feature that is only partially implemented (as of Analytica 6.5 beta). Despite being incomplete, it is in a state where it is useful for some purposes, including Lumina-internal development, and hence is documented here. It can be extremely useful, but may be subject to change between now and feature completion.

We designed the new Namespaces feature to preserve backwards compatibility. You should be able to load an existing legacy model that never used namespaces just fine.

What is a Namespace?

A global identifier is a name for a global object. Each global object belongs to a namespace and within that namespace, its identifier must be unique. For example, you cannot have two objects both named X in the same namespace. In fact, if you try to create a second object named X in the same namespace, Analytica will change it to be unique by adding numbers to the end, for example by changing the new name to X1. However, you can create a second object named X in a distinct namespace,

An object with a module or library class or subclass can be configured to be a namespace. An object of one of these classes is not normally a separate namespace, so it must be explicitly configured to be so. Filed modules and libraries are automatically configured to be namespaces, and an object with class Model is automatically a namespace.

Internally, the presence of the NamespaceDefaultScope attribute on a module subclass with a value of either Public or Private is what actually determines whether a module object is a namespace. Modules or libraries without this attribute are not namespaces, whereas those with this attribute are. The attribute value must be either Public, Private or Not A Namespace, with the meaning of this value is described below.

The identifier of a namespace module belongs to its parent's namespace and is also visible in its own namespace. As a result, each object in a namespace has to have an identifier that is distinct from its namespace's identifier. Hence, you cannot have A::A, but you could have A::B::A, where the first A and B are nested namespaces (the last A might or might not be a namespace).

In addition to your model's namespace(s), there is also a system (or root) namespace which holds the built-in functions, system variables, etc. Analytica will usually add numbers to the end of your identifier it collides with an identifier in the system namespace, but in theory that CAN co-exist. When a system object is added to Analytica in a new release, collisions with existing legacy models sometimes arise, and in this case Analytica allows both to have the same name.

Use Cases

When you have multiple people working on the same model, it is common to partition the work into Modules. Two people may end up creating variables within their own module that have the same name, but which might not be actual duplicates of the same quantity. When you merge the work together, this results in identifier collisions which must be dealt with. One nice solution is to configure each person's module to be a namespace so that these duplicate names don't actually collide, eliminating the need to rename someone's variables.

When you create a library to be used by others, you have no way to know that eventual users will never duplicate an identifier inside your library. Hence, it makes a lot of sense for your libraries to be namespaces. If fact, this is so compelling that Analytica will do this automatically when you Add Library... or Add Module... or use a filed module. (update: this idea caused backward compatibility issues. So for now, the library author needs to explicitly configure the library to be a namespace. I leave the sentence here while experimental to show we thought seriously about this.)

In some applications, a module template is created to represent a component of some sort, and then duplicated multiple times in order to create multiple instantiations of that component. For example, a "module template" might implement a certain neural network layer, which is then duplicated and arranged into layers for a specific network. In a different application, a different module template is created for each physical component type in a manufacturing facility, and then the model of the factory's operation makes duplicates in to "draw" the factory's processes. My isolating each template instance as its own namespace, the identifiers in each can remain the same in each duplicate instance, adding to a lot of convenience and also an ability to write code that knows about internal structure.

Qualified Names

When there are multiple identifiers with the same name, each in a different namespace, you may need to disambiguate which object you want by using a qualified name in the form NS :: X. For example, suppose NS1 and NS2 are names of two distinct namespaces, each containing a different object named X. Then you could use NS1 :: X or NS2 :: X to disambiguate which object you want.

Namespace names are themselves identifiers and thus subject to the same properties of other identifiers -- i.e., two namespaces might have the same name when each is in a distinct namespace. Thus, you might use a qualified name to get to the namespace of interest, resulting in a namespace path such as: NS1 :: NS2 :: X.

The operator :: is called the namespace operator. It has the highest precedence and cannot be broken up by parentheses. It must contain an identifier, qualified identifier or nothing on its left, and an identifier on its right.

You can begin a qualified name at the root/system namespace by starting out with :: such as ::Npv( cash_flow, Time, 5%). When you start at the root, it will match system objects first, and then match objects that are in scope in your model's namespace if there is no system object with the name.

Each non-terminal component of a qualified name must be the identifier of a namespace that is in scope within the namespace identified by the path up to that point. These cannot be identifiers for modules that are not configured to be namespaces. However, there is one exception to this. When creating a new object using the typescript syntax

«className» «newIdentifier»

the «newIdentifier» can specify the parent module as the penultimate component of the path. For example,

Decision Sales :: Incentive_discount

would create a decision variable named Incentive_discount inside the module Sales, and Sales does not itself need to be a namespace.

In many cases you can use an unqualified identifier to access an object that resides in a different namespace. This is possible when the identifier is unambiguously imported or is inherited by the current namespace, which are described below.

Namespace Imports

A namespace, say NS1, can import another namespace, NS2, which makes all the public identifiers in NS2 visible from NS1 without having to use a qualified path.

You specify a namespace import by adding the imported namespace's identifier (or qualified identifier) to the receiving namespace's NamespaceImports attribute. For example, in typescript:

NamespaceImports NS1 : NS2

If NS1 imports multiple namespaces, they should be listed one on each line, e.g.,

NamespaceImports NS1 : NS2~
NS3~
NS4::NS5

In the above example, the identifiers directly in NS4 are not imported but those in NS5 are.

A NamespaceImports attribute has no effect (is ignored) when it is set for a non-namespace object.

When a child (or grand child) module is configured to be a namespace, the parent module does not automatically import the identifiers is those submodules. You must add the child module to the parent's NamespaceImports attribute in order to use the identifiers inside the submodule without a qualified path. Note that this is different from the case where the child module is not configured to be a namespace, since in that case, all the objects in the child actually belong to the parent's namespace. (Note: In the future, when namespace features are exposed on the Analytica GUI, it'll automatically add the import to the parent namespace when you first configure a module to be namespace, thus requiring you to remove the import if you don't want that. At present, since you have to configure namespaces by setting attribute values, you need to do both.).

What one namespace imports multiple other namespaces, identifier ambiguity can arise.

Namespace scope

Every global object has either public or private namespace scope. This scope determines whether the object's identifier imported when one namespace imports another namespace. The scope for a given object can be specified explicitly, or it is inherits the default scope for its own namespace.

To explicitly set the scope of a given global object (which can be of any object class), set the NamespaceScope attribute to be either Public or Private. For example, in typescript,

NamespaceScope::Internals : Private

When an object does not have its NamespaceScope attribute set, its scope is the value in its namespace's NamespaceDefaultScope attribute.

When you import from a public namespace, i.e., one whose NamespaceDefaultScope is Public, the importing namespace gets all identifiers except those explicitly marked Private.

Conversely, when you import from a private namespace, i.e., one whose NamespaceDefaultScope is Private, the importing namespace gets only those identifiers explicitly marked as Public.

The NamespaceScope attribute on the namespace object itself has a few additional ramifications. Consider a case where NS1 imports NS0. The identifier for NS0 is always in scope in NS1 for either value of NamespaceScope of NS0 However, now suppose NS2 imports NS1. In the case where NamespaceScope of NS0 is Public, the identifier NS0 is imported into NS2 (i.e., can be used from NS2 without qualifiers). In the case where NamespaceScope of NS0 is Private, the symbol NS0 is not imported into NS1 (as would be the case with any object, even a non-namespace), but in addition, the identifiers inside NS0 are not imported into NS1. Hence, in this later case, the public identifiers in NS0 are available without qualifiers in NS1 but not in NS2. This latter case matches a common scenario:

  • Model MyModel (NamespaceImports: MyLib)
    • Variable SomeVar
    • Library MyLib (NamespaceDefaultScope:Public, NamespaceImports: Internals)
      • Function F(x)
      • Module Internals (NamespaceDefaultScope:Public, NamespaceScope:Private)
        • Function PrivateF(x)

The function PrivateF is in scope from Definition of F, meaning it can be used there without any qualifiers. It could also use Internals::PrivateF, but the qualifiers would be unnecessary. However, PrivateF is not in scope from the Definition of SomeVar, but F is in scope.

Private scope does not prevent other namespaces from reaching an object, it simply controls whether a names can be used without qualifiers. Hence, SomeVar could call MyLib::Internals::PrivateF(..).

Namespace inheritance

A namespace automatically inherits all identifiers that are visible in its parent's namespace.

Since its parent's namespace automatically inherits from its parent's namespace, it means that a namespace also automatically inherits from its grand parent namespace, and so on, all the way up to the root / system namespace.

A collision of identifiers up the inheritance path is not an error. For example, if X is in scope in the parent namespace, and a different X is in scope in the grandparent's namespace, then X would be ParentNS::X.

Inheritance does not apply to components of a qualified name other than the first component. For example, In the qualified name NS::X, the object X will be found if it is directly in NS or in a namespace than is imported by NS, but won't be recognized if X in inherited from a parent or ancestor namespace by NS. Within a variable definition inside NS, the unqualified identifier X is inherited, but the qualified name NS::X does not see it.

Precedence

There are three ways for an identifier to be in-scope in a namespace:

  • The object itself resides in the namespace.
  • It is imported from another namespace. This includes imports from child namespaces, as well as from unrelated namespaces.
  • It is inherited from a parent or ancestor namespace. This includes the root/system namespace at the top.

An identifier in the current namespace always takes precedence over an imported on inherited identifier with the same name. This is not considered an ambiguity or name collision.

An identifier that is imported always takes precedence over an inherited identifier with the same name. Again, this does not constitute an ambiguity (name collision).

Two identifiers with the same name that are both imported is ambiguous and constitutes a name collision. In such a situation, it would be an error to use an unqualified name. You must use a qualified name to disambiguate.

When two identifiers with the same name are both inherited, the more closely related one (with the fewest parent-namespace steps) always takes precedence. This is not considered an ambiguity.

It follows from these that system identifiers are at the lowest precedence.

When you have two identifiers with the same name that are in scope, and you want to use the one at the lower precedence, you can do so by using its qualified name.

Let's apply these to the nested namespaces A::B::A. From an object in the B namespace, an unqualified A refers to the most deeply nested A, which is in scope since it is in the same namespace. From an object in the top A namespace, an unqualified A refers to the most deeply nested A if namespace B is imported by the top namespace, but if not, then an unqualified A refers to the top namespace A.

Namespace-related attributes

You can set these attributes:

NamespaceDefaultScope
Specifies that a module object is also a namespace. Its value specifies the namespace scope of all objects in the namespace that don't themselves have an explicit NamespaceScope attribute set.
  • It is meaningful only on a Module or library object, or other subclass (module subclasses include Module Library LinkModule, LinkLibrary, Model, Form, SysLibrary, PluginLibrary). Its value has no effect (is ignored) on any other object type.
  • Recognized values: Public, Private, Not a namespace.
  • Determines whether a module is also a namespace. It is if it has a value of Public or Private.
  • The value determines the namespace scope for all objects inside the namespace who don't have their own NamespaceScope attribute set.
NamespaceScope
Specifies whether the object's identifier is exported to a remote namespace that imports the object's own namespace.
  • Recognized values: Public, Private
  • When not set, the namespace scope is taken from its namespace's NamespaceDefaultScope attribute value.
  • Public scope means its identifier is exported to an importing namespace.
  • Private means its identifier is not exported to the importing namespace.
  • On a namespace object, the above applies, but also Private means that identifiers in its namespace are not "re-exported" when a remote namespace imports a namespace that imported it.
NamespaceImports
Specifies a list of other namespaces that are imported into this namespace.
  • Only meaningful on a namespace module. Ignored otherwise.
  • List one namespace name on each line. The identifiers used must be in context from the namespace's own scope, and if not, you can use a qualified name.
  • There is no need to import parent or ancestor modules, since they are implicitly inherited.

These attributes are used internally, and are read-only:

NamespaceParsedImports
A parsed version of the NamespaceImports attribute.
NamespaceExportsTo
The inverse of the NamespaceParsedImports attribute.

Current limitations

The namespace feature is only partially implemented at present, with several limitations in its current form, some of which are listed here.

The feature will remain as experimental only until these are addressed.

Incomplete dependency maintenance

When you namespace configuration changes, it does not currently update nor invalidate everything that are impacted by the change. In many cases, you may need to save and re-open your model to see the correct impacts.

One example is that in an expression that has already been parsed. an identifier has been already resolve to its corresponding object in the parse tree. When you introduce a new namespace, remove a namespace, or change an import, it is possible that the syntax in the expression in its present form no longer maps to the same object. It might become ambiguous where it was not ambiguous previously, a qualified name might become syntactically invalid because one of its components is no longer a namespace, or it might change which object it maps to as a result of a change in precedence orders. None of these are detected. Hence, until the expression is reparsed, it would continue to have the original object in its parse tree. Eventually we want it to detect this and rewrite the expression as required to ensure it continues to refer to the same object. When you same and reload (causing it to reparse), then suddenly it uses a different object.

Another example occurs if you move an object across a namespace boundary. The same considerations apply. Also if you add a new object with a same name.

No namespace support in the GUI

Eventually you should be able to configure a module to be a public or private namespace from the Object Window, for example, and to control the namespace scope of all objects as well. You should be able to edit the NamespaceImports from the GUI.

There is also no visual distinction yet on influence diagrams or other places to indicate when a module object is a namespace.

Typescript

While many commands in the Typescript window currently do handle namespaces, refinement is still necessary. It can be a bit challenging to access objects that aren't in scope, and when identifiers are printed, it can be hard to tell which object is intended.

Crash recovery

In some cases, crash recovery may not recover unsaved changes correctly when namespaces affect name scoping or precedence.

GraphSetup

The settings in GraphSetup are likely to get confused by namespace scoping or ambiguity.

Expressions and functions

Several built-in functions need to be updated with namespace support. These include FindObjects, Aggregate, MdTable, and others.

It may also be convenient to introduce a few new built-in functions specifically for getting information about namespaces.

Error reporting

Errors messages related to ambiguities or objects not found because they are out-of-scope are not yet very informative.

Expression assist

Identifier autocomplete is not yet aware of namespace scope.

ADE

The methods of ADE classes are not yet aware of namespaces.

Comments


You are not allowed to post comments.