.. currentmodule:: halerium.core =============== Scoping Details =============== Structures ========== Halerium structures are built out of graphs, entities and variables. The building blocks can be nested to build deep and hierarchichal structures in a convenient way with the scoping mechanism. Scoping ======= Scope ----- A *scope* is the context in a halerium structure. Scopes are managed via entering and exiting python `with` blocks. Example: :: a = Graph("a") b = Graph("b") # here the current scope is `noScope` with a: # here the current scope is `a` pass with b: # here the current scope is `b` pass Scopetor -------- A *scopetor* is an object that can provide such a scope. The halerium scopetor classes are :class:`~Graph`, :class:`~Entity`, :class:`~Variable`, and :class:`~StaticVariable`. So in the above example ``a`` and ``b`` could have been instances of any of these classes. Additionally, every :class:`~Graph` instance comes with an instance of :class:`~graph.Inputs` and :class:`~graph.Outputs`. These are scopetors as well :: g = Graph("g") with g: with g.inputs: # the current scope is `g.inputs` pass with g.outputs: # the current scope is `g.outputs` pass Scopee and child ---------------- A *scopee* is an object that lives in a scope. :: >>> g = Graph("g") >>> with s: >>> e = Entity("e") # `e` now lives in `g`. So the scope of `e` is `g` >>> print(e.scope is g) True All scopetors can also scopees as well as all operators. If a scopee is also a scopetor it is referred to as a *child* of its scope. :: >>> g = Graph("g") >>> with s: >>> e = Entity("e") # `e` is now a child of `g`. >>> print(e in g.children.values()) True All children of a scopee can be accessed as properties of the scopetor by their name. :: g = Graph("g") with g: e = Entity("e") with e: v = Variable("v") >>> g.e.v is v True Graphs start with two children, ``inputs`` and ``outputs`` :: >>> g = Graph("g") >>> print(g.inputs.name) inputs >>> print(g.outputs.name) outputs Allowed combinations -------------------- Not all scopetor/scopee combinations are allowed. There is a hierarchy. Graphs can contain everything, entities can contain everything except graphs and variables can only contain variables and operators. Inputs and outputs can only contain entities. +------------------------+---------------------------------------------+ | **Scopetor** | **Scopee** | | +------------+----------+----------+----------+ | | Graph | Entity | Variable | Operator | +------------------------+------------+----------+----------+----------+ | Graph | ✓ | ✓ | ✓ | ✓ | +------------------------+------------+----------+----------+----------+ | Entity | ✗ | ✓ | ✓ | ✓ | +------------------------+------------+----------+----------+----------+ | Variable/StaticVariable| ✗ | ✗ | ✓ | ✓ | +------------------------+------------+----------+----------+----------+ | Inputs/Outputs | ✗ | ✓ | ✓ | ✗ | +------------------------+------------+----------+----------+----------+ Global and relative names ------------ The global name of a Scopee shows the full hierarchy of scope names separated by '/' characters. :: >>> g = Graph("g") >>> with g: >>> e = Entity("e") >>> with e: >>> v = Variable("v") >>> with v: >>> w = StaticVariable("w") >>> print(w.global_name) g/e/v/w The relative name is the difference between the global name of a (grand-)parent and a (grand-)child. :: >>> e.get_get_relative_name("g/e/v/w") v/w To fo from global or relative name to object use the ``get_child_by_name`` method of scopetors. :: >>> e.get_child_by_name("g/e/v/w", name_is_relative=False) is w True >>> g.get_child_by_name("v/w", name_is_relative=True) is w True Namespace convenience ===================== Halerium automatically puts references to children of a scopetor into the python namespace when the scope is entered and removes them if the scope is left. :: g = Graph("g") with g: Entity("e") >>> with g: >>> print(e) >>> print(e) NameError: name 'e' is not defined This mechanism is applied as soon as a scopee is created. :: >>> g = Graph("g") >>> with g: >>> Entity("e") >>> print(e) Sibling references are removed when a deeper scope is entered :: >>> g = Graph("g") >>> with g: >>> Entity("e") >>> Entity("d") >>> with d: >>> print(g) # exists >>> print(d) # exists >>> print(e) # was removed when `d` was entered NameError: name 'e' is not defined and restored when the deeper scope is left. :: >>> g = Graph("g") >>> with g: >>> Entity("e") >>> Entity("d") >>> with d: >>> # print(e) # would cause a NameError >>> print(e) # works Within a scope the references will trump user assigned variables, :: >>> g = Graph("g") >>> with g: >>> e = 1. >>> Entity("e") >>> print(e) but as soon as the scope is left all user assigned variables are restored. :: >>> g = Graph("g") >>> with g: >>> e = 1. >>> Entity("e") >>> print(e) 1.0 Remarks about namespace references ---------------------------------- Note that the references that halerium places into the namespace are not the actual objects, but special references to them. These references redirect all function calls etc. to the actual object. As a consequence the python built-ins ``is`` and ``type`` will distinguish between the actual object and the reference. :: >>> g = Graph("g") >>> with g: >>> Entity("e") >>> print(g.e is e) False >>> print(type(g.e)) >>> with g: >>> print(type(e)) For all other purposes the reference and the actual object are the same thing.