Swapping Dependencies

Claro's Module system was very carefully designed to guarantee that it's statically impossible for two separate modules to be "tightly coupled". In this section we'll dive into exactly what that means.

As you've already seen in previous sections, Claro Modules explicitly declare a public API that indicates the full set of procedures/values/Types that the Module's consumers will gain access to. Of course, some form of this is present in every language. The unique distinction is that Claro Module dependencies can be directly swapped out to any other Module with an appropriate API without changing a single line of code in any .claro source files.

For example, the below API...

Fig 1:


# get_message.claro_module_api
provider getMessage() -> string;

...could be implemented by multiple Modules...

Fig 2:


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

claro_module(
    name = "hello_world",
    module_api_file = "get_message.claro_module_api",
    srcs = ["hello_world.claro"],
)
claro_module(
    name = "look_ma",
    module_api_file = "get_message.claro_module_api",
    srcs = ["look_ma.claro"],
)
# ...

...and then the exact same .claro source code...

Fig 3:


# example.claro
print(Msg::getMessage());

...could be compiled against either Module...

Fig 4:


# BUILD
claro_binary(
    name = "test",
    main_file = "example.claro",
    deps = {
        "Msg": ":hello_world",
    }
)

Fig 5:


[0.001s][warning][perf,memops] Cannot use file /tmp/hsperfdata_runner/3 because it is locked by another process (errno = 11)
Hello, World!

...and the behavior would depend on which dependency was chosen...

Fig 6:


# BUILD
claro_binary(
    name = "test",
    main_file = "example.claro",
    deps = {
        "Msg": ":look_ma",  # Swapped for `:hello_world`.
    }
)

Fig 7:


[0.002s][warning][perf,memops] Cannot use file /tmp/hsperfdata_runner/3 because it is locked by another process (errno = 11)
----------------------
| Look ma, no hands! |
----------------------

Dep Validity is Based on Usage

The other subtle point that's likely easy to miss if it's not pointed out explicitly is that the validity of a Module dependency is completely dependent upon the usage of the dependency. In less opaque terms, this just means that a Module dependency is valid if the Module's API actually exports everything that is used by the consuming code. The consuming code doesn't make any constraints on anything other than what it actually uses. So, a dependency can be swapped for another that actually exports a completely different API, so long as it at least exports everything that the consuming code actually used from the original Module's API.

For example, if a third Module actually implemented a totally different API such as:

Fig 8:


# extra_exports.claro_module_api
provider getMessage() -> string;

provider getMessageFromDB() -> future<string>;

opaque newtype SecretMessage

static SOME_OTHER_MESSAGE : SecretMessage;

the dependency would still be valid because example.claro only actually uses the getMessage(...) procedure that is exported by both :look_ma and :hello_world.

This single design decision actually enables a huge amount of Build time configurability options. If you'd like to see more about how you can take advantage of this, read about how you can swap dependencies programmatically using Build flags. And if you're interested in becoming a power-user, this feature can be leveraged in some very powerful ways via Build Time Metaprogramming.