Module System

All but the most trivial programs will require some mechanism for decomposing a larger program into smaller, reusable components that can be composed into a larger whole. In Claro projects, this is accomplished via the Module System whereby distinct functionality can be organized logically to facilitate encapsulation. In addition, Claro's Module System is the source of Claro's build incrementality - modules are compiled in isolation, allowing caching such that modules do not need to be recompiled unless its own or its dependencies implementations have changed.

Defining a Module

A Module exposes an API that is implemented by some set of source files which may depend on other modules.

API

Module APIs are explicitly defined using a .claro_module_api file that will list exported procedure signatures, type declarations, static values, and Contract implementations that are publicly exposed to consumers that place a dependency on this module.

Fig 1:


# example.claro_module_api

contract Numeric<T> {
  function add(lhs: T, rhs: T) -> T;
  function multiply(lhs: T, rhs: T) -> T;
}

newtype Foo : int
implement Numeric<Foo>;

consumer prettyPrint(lhs: Foo);

Sources

An API alone simply defines an interface that the module will satisfy in its implementation sources. So implementations must be provided in the form of one or more .claro files. The above API could be satisfied by the below implementation files (note: this could be done in a single source file, but here it's split into multiple just as an example):

Fig 2:


# contract_impl.claro

implement Numeric<Foo> {
  function add(lhs: Foo, rhs: Foo) -> Foo {
    return Foo(unwrap(lhs) + unwrap(rhs));
  }
  function multiply(lhs: Foo, rhs: Foo) -> Foo {
    return Foo(unwrap(lhs) * unwrap(rhs));
  }
}

Fig 3:


# pretty_print.claro

consumer prettyPrint(f: Foo) {
  unwrap(f)
    |> "Foo: {^}"
    |> Boxes::wrapInBox(^)  # <-- Calling dep Module function.
    |> print(^);
}

Dependencies

While Modules are intended to be consumed as a reusable component, it may also itself depend on other modules in order to implement its public API.

Notice that the implementation of prettyPrint above makes a call to Boxes::wrapInBox(...). This is an example of calling a procedure from a downstream dep Module in Claro. In order to build, this Module must place a dep on some Module that has at least the following signature in its API: function wrapInBox(s: string) -> string;. As you'll see below, this Module will choose to give that downstream dependency Module the name Boxes, but any other name could've been chosen.

Dependency Naming: While consumers are allowed to pick any name they want for Modules that they depend on, it should be noted that Claro will adopt the convention that all non-StdLib Module names must begin with an uppercase letter. All StdLib Modules will be named beginning with a lowercase letter. This is intended to allow the set of StdLib modules to expand over time without ever having to worry about naming collisions with user defined Modules in existing programs.

Static enforcement of this convention hasn't been implemented yet, but just know that it's coming in a future release.

Defining BUILD Target

A Claro Module is fully defined from the above pieces by adding a claro_module(...) definition to the corresponding Bazel BUILD file:

Fig 4:


# BUILD

load("@claro-lang//:rules.bzl", "claro_module")

claro_module(
    name = "example",
    module_api_file = "example.claro_module_api",
    srcs = [
        "contract_impl.claro",
        "pretty_print.claro",
    ],
    deps = {
        "Boxes": ":box",  # <-- Notice the name "Boxes" is chosen by the consumer.
    },
    # This Module can be consumed by anyone.
    visibility = ["//visibility:public"],
)

claro_module(
    name = "box",
    module_api_file = "boxes.claro_module_api",
    srcs = ["boxes.claro"],
    # No visibility declared means that this Module is private to this Bazel package.
)

# ...

Building a Module

In order to validate that a claro_module(...) target compiles successfully, you can run a Bazel command like the following:

(Assuming the BUILD file is located at //path/to/target)

$ bazel build //path/to/target:example

This will build the explicitly named target and its entire transitive closure of dependencies (assuming their build results have not been previously cached in which case they'll be skipped and the cached artifacts reused).

Executable Using Above Example Module

To close the loop, the above example Module could be consumed and used in the following executable Claro program in the following way.

Fig 5:


# BUILD

load("@claro-lang//:rules.bzl", "claro_binary")

...

claro_binary(
    name = "test",
    main_file = "test.claro",
    deps = {
        "Ex": ":example",
    },
)

Fig 6:


# test.claro

var f1 = Ex::Foo(1);
var f2 = Ex::Foo(2);

var addRes = Ex::Numeric::add(f1, f2);
Ex::prettyPrint(addRes);

var mulRes = Ex::Numeric::multiply(f2, Ex::Foo(5));
Ex::prettyPrint(mulRes);

Output:

----------
| Foo: 3 |
----------
-----------
| Foo: 10 |
-----------