Initializers & Unwrappers

Claro's builtin types are already fully capable of expressing any data structure, and so the entire purpose of User-Defined Types is to allow user code to overlay new semantic meaning onto types beyond just the raw data values themselves. Claro supports two simple constructs that allow User-Defined Types to constrain, and reinterpret the raw data types that they wrap. Note that both of these constructs should likely only be used in limited cases where you have a very specific reason to be doing so.

Initializers

Initializers provide a mechanism for a User-Defined Type to constrain the domain of possible values that a type may represent beyond what the raw data types imply on their own.

To demonstrate the problem being addressed, take for example the type declaration below:

Fig 1:


# ex1-no-init.claro_module_api
newtype OddInt : int

There's nothing about the type definition alone that actually imposes any sort of constraint that actually guarantees that the wrapped int is in fact odd. So a consumer could place a dep (Nums) on the Module and directly construct a completely invalid instance of the OddInt type:

Fig 2:


# BUILD
load("//:rules.bzl", "claro_binary", "claro_module")

claro_module(
    name = "ex1_no_initializer",
    module_api_file = "ex1-no-init.claro_module_api", # `newtype OddInt : int`
)

claro_binary(
    name = "bad_init_example",
    srcs = "ex1-bad-init.claro",
    deps = {
        "Nums": ":ex1_no_initializer",
    }
)

Fig 3:


# ex1-bad-init.claro
var invalidOddInt = Nums::OddInt(8);
print(invalidOddInt);

Output:

OddInt(8)

Of course, it'd be very much preferable for it to be impossible to ever construct an instance of a Type that violates its semantic invariants. You can enforce this in Claro by defining Initializers over the Type. Initializers are simply procedures that become the only procedures in the entire program that are allowed to directly use the Type's constructor. Therefore, if a Type declares an initializers block, the procedures declared within become the only way for anyone to receive an instance of the type.

Fig 4:


# ex1-with-init.claro_module_api
newtype OddInt : int

atom NOT_ODD
initializers OddInt {
  function getOddInt(x: int) -> oneof<OddInt, std::Error<NOT_ODD>>;
}

Fig 5:


# BUILD
load("//:rules.bzl", "claro_binary", "claro_module")

claro_module(
    name = "ex1_with_initializer",
    module_api_file = "ex1-with-init.claro_module_api",
)

claro_binary(
    name = "rejected_init_example",
    srcs = "ex1-bad-init.claro", # Same as before. We'll expect an error.
    deps = {
        "Nums": ":ex1_with_initializer", # Now defines an Initializer.
    }
)

Now, the exact same attempt to construct an invalid instance of OddInt is statically rejected at compile-time - and even better, Claro's able to specifically recommend the fix, calling the Nums::getOddInt(...) function:

Fig 6:


# ex1-bad-init.claro
var invalidOddInt = Nums::OddInt(8);
print(invalidOddInt);

Compilation Errors:

initializers_and_unwrappers_EX6_example.claro:2: Illegal Use of User-Defined Type Constructor Outside of Initializers Block: An initializers block has been defined for the custom type `[module at //mdbook_docs/src/module_system/module_apis/type_definitions/initializers_and_unwrappers:ex1_with_initializer]::OddInt`, so, in order to maintain any semantic constraints that the initializers are intended to impose on the type, you aren't allowed to use the type's default constructor directly.
		Instead, to get an instance of this type, consider calling one of the defined initializers:
			- Nums::getOddInt
var invalidOddInt = Nums::OddInt(8);
                    ^^^^^^^^^^^^^^^
1 Error

Note: Claro's error messaging is a work in progress - the above error message will be improved.

And now finally, you can use the initializer by simply calling it like any other procedure:

Fig 7:


# ex1-good-init.claro
var invalidOddInt = Nums::getOddInt(8);
print(invalidOddInt);

var oddInt = Nums::getOddInt(7);
print(oddInt);

Output:

Error(NOT_ODD)
OddInt(7)

Now you know for a fact that anywhere where you initialize an instance of an OddInt in the entire program, it will certainly satisfy its semantic invariants.

Warning: Still, keep in mind that if your type is mutable, declaring Initializers is not, on its own, sufficient to guarantee that any constraints or invariants are maintained over time. Keep reading to learn about how Unwrappers and Opaque Types can give you full control over this.