Control, Scope And Visibility

In Aqua, the default namespace of a module is the file name and all declarations, i.e., data, services and functions, are public.

For example, the default.aqua file:

-- default_foo.aqua


func foo() -> string:
    <- "I am a visible foo func that compiles"

Which we compile with

aqua -i aqua-scripts -o compiled-aqua

to obtain Typescript wrapped AIR, default_foo.ts in the compiled-aqua directory:

import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence';
import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable';
import { RequestFlow } from '@fluencelabs/fluence/dist/internal/RequestFlow';


// Services


// Functions

export async function foo(client: FluenceClient, config?: {ttl?: number}): Promise<string> {
    let request: RequestFlow;
    const promise = new Promise<string>((resolve, reject) => {
        const r = new RequestFlowBuilder()
            .disableInjections()
            .withRawScript(
                `
(xor
 (seq
  (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
  (xor
   (call %init_peer_id% ("callbackSrv" "response") ["I am a visible foo func that compiles"])
   (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
  )
 )
 (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)

            `,
            )
            .configHandler((h) => {
                h.on('getDataSrv', '-relay-', () => {
                    return client.relayPeerId!;
                });

                h.onEvent('callbackSrv', 'response', (args) => {
    const [res] = args;
  resolve(res);
});

                h.onEvent('errorHandlingSrv', 'error', (args) => {
                    // assuming error is the single argument
                    const [err] = args;
                    reject(err);
                });
            })
            .handleScriptError(reject)
            .handleTimeout(() => {
                reject('Request timed out for foo');
            })
        if(config && config.ttl) {
            r.withTTL(config.ttl)
        }
        request = r.build();
    });
    await client.initiateFlow(request!);
    return promise;
}

Regardless of your output target, i.e. raw AIR or Typescript wrapped AIR, the default module namespace is default_foo and foo is the compiled function.

While this default approach is handy for single file, single module development, it makes for inefficient dependency management and unnecessary compilations for multi-module projects. The remainder of this section introduces the scoping and visibility concepts available in Aqua to effectively manage dependencies.

Managing Visibility With module and declare

By default, all declarations in a module, i.e., data, service and func, are public. With the module declaration, Aqua allows developers to create named modules and define membership visibility where the default visibility of module is private. That is, with the module declaration all module members are private and do not get compiled.

Let's create an export.aqua file like so:

When we compile export.aqua

nothing gets compiled as expected:

You can further check the output directory, compiled-aqua, in our case, for the lack of output files. Consequently, foo cannot be imported from other files. For example:

Results in compile failure since foo is not visible to import.aqua:

We can use declares to create visibility for a module namespace for consuming modules. For example,

in and by itself does not result in compiled Aqua:

But once we link from another module, e.g.:

We get the appropriate result:

in form of import.ts:

Of course, if we change import.aqua to include the private bar:

We get the expected error:

As indicated in the error message, declares * makes all members of the namespace public, although we can be quite fine-grained and use a comma separated list of members we want to be visible, such as declares foo, bar.

Scoping Inclusion With use and import

We already encountered the import statement earlier. Using import with the file name, e.g., import "export.aqua", imports all visible, i.e., public, members from the dependency. We can manage import granularity with the from modifier, e.g., import foo from "file.aqua", to limit our imports and subsequent compilation outputs. Moreover, we can alias imported declarations with the as modifier, e.g.,import foo as HighFoo, bar as LowBar from "export_file.aqua".

In addition to import, we also have the use keyword available to link and scope. The difference betweenuse and import is that use brings in module namespaces declared in the referenced source file. For example:

declares the ExportModule namespace and makes foo visible. We can now bring foo into scope by means of its module namespace ExportModule in our import file without having to (re-) declare anything:

This example already illustrates the power of use as we now can declare a local foo function rather than the foo_wrapper we used earlier. use provides very clear namespace separation that is fully enforced at the compiler level allowing developers to build, update and extend complex code bases with clear namespace separation by default.

The default behavior for use is to use the dependent filename if no module declaration was provided. Moreover, we can use the as modifier to change the module namespace. Continuing with the above example:

Last updated

Was this helpful?