Reusing Source Code

Continuing to consider the "Animals" example from the previous section, let's consider a simple refactoring.

As a reminder, previously, calls to AnimalSounds::makeNoise(...) produced very simple output:

Fig 1:


# animals_example.claro
var animals: [oneof<Cat::State, Dog::State>] = [
    Dog::create("Milo", true),
    Dog::create("Fido", false),
    Cat::create("Garfield", "This is worse than Monday morning.")
  ];

for (animal in animals) {
  print(AnimalSounds::AnimalSounds::makeNoise(animal));
}

Output:

Woof!
Grrrr...
This is worse than Monday morning.

As currently defined...

Fig 2:


# BUILD
load(":animals.bzl", "Animal")

Animal(name = "dog", srcs = ["dog.claro"])
Animal(name = "cat", srcs = ["cat.claro"])

...if we wanted to include the animals' names in the printed lines. We'd have to go and manually update each Module's implementation, making changes to both dog.claro and cat.claro (and importantly, to any other animals we'd want to share this updated behavior):

Fig 3:


# cat.claro
newtype InternalState : struct { favoriteInsult: string }

implement AnimalSounds::AnimalSounds<State> {
  function makeNoise(cat: State) -> string {
    # Cats are mean, they're going to say mean things no matter what.
    var noise = unwrap(unwrap(cat).internal).favoriteInsult;
    return "{noise} - says {unwrap(cat).name}"; # Analogous code repeated in dog.claro.
  }
}

function create(name: string, favoriteInsult: string) -> State {
  return State({
    name = name,
    internal = InternalState({favoriteInsult = favoriteInsult})
  });
}

And now, after making the changes, rerunning will give us the updated output we were looking for:

Fig 4:


Woof! - says Milo
Grrrr... - says Fido
This is worse than Monday morning. - says Garfield

Repetition may be ok in some situations, but in many others, it would represent a risk of potential maintenance costs.

Of course, you could always factor out the common logic into a new Module that can be depended on and called explicitly by each animal implementation (and in fact, this is absolutely the recommended approach in most situations). But, since we're interested in digging into possible Build time metaprogramming capabilities in this section, by way of example, we'll walk through some other ways you could go about sharing this base implementation across Modules that potentially wouldn't have been immediately obvious, coming from other languages.

claro_module(...) Accepts Multiple Srcs

The first thing to understand is that a Module's implementation can be spread across multiple source files. This means that different .claro files can satisfy different portions of a Module's API. And, more importantly for our current purposes, this means that instead of creating a whole new Module to contain the common logic factored out of dog.claro and cat.claro, we could instead define a single new file containing that factored out logic...

Fig 5:


# get_message_with_name.claro
function getMessageWithName(message: string, state: State) -> string {
  var name = unwrap(state).name; # All animal States have a top-level `name` field.
  return "{message} - says {name}";
}

...include it in the srcs of BOTH Module declarations...

Fig 6:


# BUILD
load(":animals.bzl", "Animal")

# An example of **LITERALLY** reusing code.
Animal(name = "dog", srcs = ["dog.claro", "get_message_with_name.claro"])
Animal(name = "cat", srcs = ["cat.claro", "get_message_with_name.claro"])

...and then directly call the factored out function in each Module's implementation!

Fig 7:


# cat.claro
newtype InternalState : struct { favoriteInsult: string }

implement AnimalSounds::AnimalSounds<State> {
  function makeNoise(cat: State) -> string {
    var noise = unwrap(unwrap(cat).internal).favoriteInsult;
    return getMessageWithName(noise, cat); # Analogous code repeated in dog.claro.
  }
}

function create(name: string, favoriteInsult: string) -> State {
  return State({
    name = name,
    internal = InternalState({favoriteInsult = favoriteInsult})
  });
}

This is an example of LITERAL code reuse - something that's generally not actually possible in other languages. In fact, you could take this a step further by factoring out this shared src file directly into the Animal(...) Macro implementation to automatically make the getMessageWithName(...) function available to all Animal(...) declarations.

The key to this all working is that when the reused function references the State Type, it refers to either Dog::State or Cat::State depending on the context in which it's compiled. And the only field accessed via unwrap(state).name is valid for both types. In a sense, this form of Build time metaprogramming has given this strongly, statically typed programming language the ability to drop down into dynamic "duck typing" features when it's convenient to us. This utterly blurs the lines between the two typing paradigms while still maintaining all of the static type validations because all of this is happening at Build time, with Compile time's type-checking validations still to follow!

"Inheritance" - Inverting the Prior Example

The prior example is a demonstration of the "composition" model where, in order to share code, you explicitly compose new code around the shared code by manually calling into the shared code.

But, of course, while composition is generally recommended over the inverted "inheritance" model, many people prefer the convenience that inheritance-based designs offer. Specifically, as you saw in the prior example, composition is more verbose, as you have to explicitly opt in to code sharing, whereas inheritance makes this implicit.

Now, instead of each Module implementing the AnimalSounds Contract manually, a single default implementation could be written...

Fig 8:


# default_animal_sounds_impl.claro
implement AnimalSounds::AnimalSounds<State> {
  function makeNoise(state: State) -> string {
    return "{makeNoiseImpl(state)} - says {unwrap(state).name}";
  }
}

...and then each Animal Module simply needs to define the expected internal implementation function makeNoiseImpl(...) to provide its custom logic...

Fig 9:


# cat.claro
newtype InternalState : struct { favoriteInsult: string }

function makeNoiseImpl(cat: State) -> string {
  # No more code duplication.
  return unwrap(unwrap(cat).internal).favoriteInsult;
}

function create(name: string, favoriteInsult: string) -> State {
  return State({
    name = name,
    internal = InternalState({favoriteInsult = favoriteInsult})
  });
}

...and again, the "inherited" code can be included in the srcs of BOTH Module declarations...

Fig 10:


# BUILD
load(":animals.bzl", "Animal")

# An example of **LITERALLY** reusing code.
Animal(name = "dog", srcs = ["dog.claro", "default_animal_sounds_impl.claro"])
Animal(name = "cat", srcs = ["cat.claro", "default_animal_sounds_impl.claro"])

Modern software engineering best practices have been progressing towards the consensus view that you should prefer composition over inheritance. But, even though this preference is generally shared by Claro's author, it shouldn't necessarily indicate that inheritance is impossible to achieve. While Claro won't ever add first-class support for inheritance to the language, Claro explicitly leaves these sorts of design decisions to you and provides Build time metaprogramming support to allow the community to encode these sorts of organizational design patterns themselves to be available for whoever decides they have a good reason for it. You shouldn't need to be hostage to the language designer's agreement or prioritization to be able to extend the code organization patterns that can be expressed in the language.

Further Flexibility

If you've made it this far, well done! You may never need to use these "power user" features, but you should now have the core conceptual background that you'll need to use Bazel to encode your own relatively sophisticated custom organizational design patterns in your Claro programs using Build time metaprogramming!

Of course, there's always another step deeper into such waters. By continuing on to the next section, we'll continue to develop the Animals example even further. In particular, we'll demonstrate one such sophisticated design pattern called "Abstract Modules" that fully generalizes all of the functionality described in the past two sections, and goes even further to provide significant configurability controls on top of what you've seen in the example Animal(...) macro so far.