Skip to content

Blocks and scope

Paul M Fox edited this page Nov 9, 2016 · 1 revision

The scope of variables and mixins in SCL is determined by block structure. Each block defined clones its parent scope by copying references to variables and mixins into a new scope. New mixins and variables declared in the scope will not be accessible to the parent, but will be passed down to child block scopes. Variables assigned values in a child scope will have their values changed in the parent scope because all variables are cloned between scopes by reference.

That's a confusing concept in the abstract, and the difficulty is compounded by the fact that variables and mixins have slightly different properties when it comes to scope. Let's dig into the differences.

Variables

All variables are transferred between scopes by reference, meaning that if you were to create a variable in a parent block and then assign it a new value in a child block, the value would be changed in both scopes. In code, that might look like this:

$myVariable := 1

block
    // This is a new scope, containing a reference
    // to $myVariable, which can be re-assigned.
    $myVariable = 2

    // The value of $myVariable is now `2`
    someThing = $myVariable

    anotherBlock
        // This is *another* new block, which contains
        // a reference to the original $myVariable
        $myVariable = 3

// The value of $myVariable is now `3`,
// and that value can be assigned to someThing
someThing = $myVariable

The nesting level of the scopes doesn't matter: as long as $myVariable is not recreated (that is, with the := operator) it's cloned as a reference, and it can be re-assigned at any depth. In diagrammatic form, the flow of the variable scope might be clearer:

<root>
|-- create [$myVariable] -->----------------------+
|-- block                                         |
|   |-- assign [$myVariable] -->------------------|
|   |-- declaration using [$myVariable] --<-------|
|   +-- anotherBlock                              |
|       +-- assign [$myVariable] -->--------------|
+-- declaration using [$myVariable] --<-----------+

Creating a new variable in a scope, even if it has the same name as a variable in the parent scope, breaks the reference. Consider the following example:

$myVariable := 1

block
    // This is a new block with a new variable
    $myVariable := 2

    // The value of this scope's $myVariable is `2`
    someThing = $myVariable

    anotherBlock
        // This is another new block, which contains
        // a reference to its parent's $myVariable
        $myVariable = 3

# The value of $myVariable is still `1`
someThing = $myVariable

The parent scope instance of $myVariable was never referenced by the block (which created its own version with the same name) and thus wasn't assigned a value in the block.

The scope diagram for that code looks like this:

<root>
|-- create [$myVariable] -->----------------------+
|-- block                                         |
|   |-- create [$myVariable] -->---------------+  |
|   +-- declaration using [$myVariable] --<----|  |
|   +-- anotherBlock                           |  |
|       +-- assign [$myVariable] -->-----------+  |
+-- declaration using [$myVariable] --<-----------+

Again, there's no limit to the nesting level of variables, and any variable created will continue to cascade down the scope chain until it's replaced by a new variable of the same name.

Mixins

Mixins are also copied between scopes. They naturally cascade down through the scopes in the same way as variables. The following code should be completely intuitive:

@myMixin()
    something = 1

block
    anotherBlock
        // myMixin can be called here because it's transferred down the scopes
        myMixin()

And, like variables, mixins can be replaced by new mixin declarations:

@myMixin()
    something = 1

block
    // This will replace the parent myMixin
    @myMixin()
        something = 2

    anotherBlock
        // The myMixin called here is the one declared
        // on line 6, not the one on line 1
	    myMixin()

Combined effects for sophisticated mixins

When __body__() is used in a mixin (see the mixins documentation for details) the scope of the calling block and the mixin's active scope at the time of the __body__() call are merged, with the mixin's stack taking priority when there's a conflict on variables, and the call stack taking priority when there's a conflict on mixins.

This provides for two cornerstone strategies for complex mixins, like those used in the standard library. The first is that that mixin call body blocks have access to all variables in both stacks.

To illustrate, consider this complex example:

@myMixin($v1)
    $v2 = 2
    __body__()

block
    anotherBlock
        $v3 := 3
	    myMixin(1)
            v1 = $v1 // passed to myMixin on line 8; its value is 1
            v2 = $v2 // set in mixin on line 2; its value is 2
            v3 = $v3 // declared in the caller's parent scope (line 8); value 3

The second is that, becase child mixins of mixins are merged into the call scope favouring the caller, it's possible to have simple 'overloading' in mixin sets.

The following code is non-trivial, even though it may look simple:

@masterMixin()
    @childMixin1()
        something = 1
    @childMixin2()
        something = 2
    __body__()

masterMixin:
   @childMixin2()
        something = 3
   childMixin1()
   childMixin2()

The resultant HCL, if you're wondering, is this:

something = 1
something = 3

Clone this wiki locally