Static Values

In addition to Type definitions and Procedure signatures, Modules are also able to export static (read: unchanging) values. This seemingly simple feature actually addresses the core value add of heavyweight "Dependency Injection" frameworks like Guice, Dagger, or Spring1 while providing the static compile-time validation that you'd expect of a first-class programming language feature.

The below Module API exports a struct containing a simple server config that's fixed (static) throughout the server's lifecycle:

Fig 1:


# ex1.claro_module_api

# Throughout the server's lifetime this configuration won't change.
static SERVER_CONFIG : ServerConfig;

alias ServerConfig : struct {
  server_name: string,
  port: int,
  logging: struct {
    filename: string
  },
  database: struct {
    host: string,
    port: int,
    database_name: string
  }
}

The value itself will be provided by implementing a provider static_<static value name>() -> <static value type>, for example, the following provider implementation reads and parses2 the config values from a JSON resource3 file:

Fig 2:


{
  "server_name": "My Server",
  "port": 8080,
  "logging": {
    "filename": "server.log"
  },
  "database": {
    "host": "localhost",
    "port": 5432,
    "database_name": "my_database"
  }
}

Fig 3:


# ex1-impl.claro
provider static_SERVER_CONFIG() -> ServerConfig {
  resources::ConfigJSON
    |> files::readOrPanic(^)
    |> var parsedConfig: std::ParsedJson<ServerConfig> = fromJson(^);

  var parsedResult = unwrap(parsedConfig).result;
  if (parsedResult instanceof ServerConfig) {
    # Config has correct format and was parsed successfully.
    return parsedResult;
  }
  # ... Invalid Config File Format Handling ...
  # TODO(steving) In the future Claro should support an explicit `Panic("Reason")`.
  # TODO(steving) This server shouldn't even be allowed to actually startup.
  return {
    server_name = "Fake Server",
    port = -1,
    logging = {
      filename = "server.log"
    },
    database = {
      host = "localhost",
      port = -1,
      database_name = "Fake Database"
    }
  };
}

This syntax is very likely to change. Expressing this via a naming convention is extremely undesirable, so any suggestions for a more appropriate syntax are very welcome.

And now, a downstream dependent of the Module exporting the SERVER_CONFIG static value can just directly use the value as it was initialized at program startup by the given provider.

Fig 4:


var config = Config::SERVER_CONFIG;
print("Server Name: {config.server_name}");
print("Port:        {config.port}");

Output:

Server Name: My Server
Port:        8080

Static Values Must be Deeply Immutable

The primary restriction placed on Static Values is that they must be deeply immutable to prevent static values from being used in such a way could lead to data races. Because Static Values can be directly referenced anywhere in your program, this means they can be referenced directly or transitively by Graph Procedures or by Lambdas directly scheduled to execute off the main thread using the StdLib's futures module. This must be prevented in order to keep with Claro's philosophy of making it impossible for two threads to share mutable state.

Initialization Order

In general, Static Values are initialized on program startup4 before a single line of the "main file" (determined by claro_binary(name = ..., main_file = ..., deps = ...)) ever actually ran. To demonstrate this, let's add a print(...) statement to both the Static Value's provider, and to the main file that references it:

Fig 5:


# ex1-impl.claro
provider static_SERVER_CONFIG() -> ServerConfig {
  log("STATIC VALUE INITIALIZATION");
  # ...
  resources::ConfigJSON
    |> files::readOrPanic(^)
    |> var parsedConfig: std::ParsedJson<ServerConfig> = fromJson(^);

  var parsedResult = unwrap(parsedConfig).result;
  if (parsedResult instanceof ServerConfig) {
    # Config has correct format and was parsed successfully.
    return parsedResult;
  }
  # ... Invalid Config File Format Handling ...
  # TODO(steving) In the future Claro should support an explicit `Panic("Reason")`.
  # TODO(steving) This server shouldn't even be allowed to actually startup.
  return {
    server_name = "Fake Server",
    port = -1,
    logging = {
      filename = "server.log"
    },
    database = {
      host = "localhost",
      port = -1,
      database_name = "Fake Database"
    }
  };
}
consumer log(msg: string) {
  print("LOG: {msg}");
}

Fig 6:


Config::log("START MAIN FILE");

var config = Config::SERVER_CONFIG;
print("Server Name: {config.server_name}");
print("Port:        {config.port}");

Output:

LOG: STATIC VALUE INITIALIZATION
LOG: START MAIN FILE
Server Name: My Server
Port:        8080

"Lazy" Static Values

It's possible, however, that it might not be desirable for this sort of static initialization to happen eagerly like this (for example if the value isn't guaranteed to even be used). So, Claro allows static values to optionally be declared lazy:

Fig 7:


# ex1.claro_module_api

# Throughout the server's lifetime this configuration won't change.
lazy static SERVER_CONFIG : ServerConfig;

alias ServerConfig : struct {
  server_name: string,
  port: int,
  logging: struct {
    filename: string
  },
  database: struct {
    host: string,
    port: int,
    database_name: string
  }
}

which will effectively wrap every reference to the value in logic that will first check if the value still needs to be initialized and the initialization logic will be performed exactly once the very first time a read of the Lazy Static Value is actually executed at runtime:

Fig 8:


Config::log("START MAIN FILE");

var config = Config::SERVER_CONFIG;
print("Server Name: {config.server_name}");
print("Port:        {config.port}");

Output:

LOG: START MAIN FILE
LOG: STATIC VALUE INITIALIZATION
Server Name: My Server
Port:        8080

In the case of this example, lazy initialization could mean that the file read of the JSON config resource never actually needs to occur if it would never actually be read. This is a fairly insignificant performance optimization, but one that will be welcome to any developers that have become accustomed to this sort of capability being provided by more heavyweight dependency injection frameworks.

Static Value Providers May Depend on Other Static Values

Finally, it's worth explicitly noting that Static Value providers may depend on other Static Values, with the only restriction being that circular dependencies between Static Value providers are forbidden. In fact, Claro will reject them at compile time to ensure that you don't accidentally create an infinite loop during initialization.


1

Claro doesn't support these DI frameworks' concept of "scopes" explicitly, but Claro's Static Values could be conceptually considered to be in the "Singleton" scope in any of the mentioned DI frameworks.

2

Learn more about Claro's support for automatic JSON Parsing.

3

Learn more about Claro's support for Resource Files in the StdLib's files module.

4

To be very explicit, technically Static Values are instantiated the first time that the JVM's ClassLoader loads the generated Class representing the Module exporting the Static Value. Hence the calls to Config::log(...) to make the example more compelling.