Previous ... Next

Finally, Metaprogramming

Metaprogramming is the most important feature of 42. All the decorators that you have seen up to now are implemented with metaprogramming, which shows that 42 offers a good balance of freedom and safety.

The main idea of 42 metaprogramming is that only library literals can be manipulated. Metaprogramming is evaluated top down, most-nested first. Once a library literal has a name, it can not be independently metaprogrammed; but only influenced by metaprogramming over the library that contains it.

«» is a decorator allowing to store code in a reusable format. «» is a decorator that extracts the code from a trait. For example

In this code we show that «» contains all the code of both «» and «». Note how the abstract «» in «» is merged with the corresponding implemented method in «». Traits allow us to merge code from different sources, as it happens with multiple inheritance. However, traits are flattened: The code is actually copied in the result. Traits are boxes containing the code and offering methods to manipulate such code. A trait can be created by doing either «» or «». The trait object has a method «» returning the contained code. Trait content can be composed using the operator «» (as shown above) or «». For traits there is no difference in behaviour between «» and «», but the operator precedence and associativity is different.

Simply composing traits allows us to emulate a large chunk of the expressive power of conventional inheritance. For example, in Java we may have an abstract class offering some implemented and some abstract methods. Then multiple heir classes can extend such abstract class implementing those abstract methods. The same scenario can be replicated with traits: a trait can offer some implemented and some abstract methods. Then multiple classes can be obtained composing that trait with some other code implementing those abstract methods.

(2/5)Trait composition: methods

Trait composition merges members with the same name. As shown above, this allows method composition. Also nested classes can be merged in the same way: nested classes with the same name are recursively composed, as shown below:

Fields?

But what about fields? how are fields and constructors composed by traits? The answer to this question is quite interesting: In 42 there are no true fields or constructors; they are just abstract methods serving a specific role. That is, the following code declares a usable «» class:

That is, any «», «» or «» no-arg abstract method can play the role of a getter for a correspondingly named field, and any abstract «» method can play the role of a factory, where the parameters are used to initialize the fields. Finally, «» methods with one argument called «» can play the role of a setter. Candidate getters and setters are connected with the parameters of candidate factories by name. To allow for more then one getter/setter for each parameter, getters/setters names can also start with any number of «».
We call those abstract methods Abstract State Operations . In Java and many other languages, a class is abstract if it has any abstract methods. In 42, a class is coherent if its set of abstract state operations ensure that all the callable methods have a defined behaviour; this includes the initialization of all the usable getters.
In more detail, a class is coherent if:
  • All candidate factories provide a value for all candidate getters, and all the types of those values agree with the return type of the corresponding getters. The parameter type of all candidate setters agrees with the return type of the corresponding getters.
  • Additionally, any non-class method can be abstract if none of the candidate factories return a value whose modifier allows to call such a method.
In particular, this implies that if a class has no candidate factories, any non class method may be abstract, as shown below:
A main can call class methods only on coherent classes that only depend from other coherent classes, thus for example
The decorators «» and «» also checks for coherence: the following application of «»
would fail with the following message: The class is not coherent. Method bar() is not part of the abstract state . We can use «» and «» to suppress this check when needed. Indeed «» behaves exactly as «».

Earlier in this tutorial we have shown code like

In that code «» looks like a conventional field declaration, but it is simply syntactic sugar for the following set of methods:
Then, «» will discover that «», «» and «» are good candidate fields and will add factories
and a lot of other utility methods.

(3/5)Nested Trait composition: a great expressive power.

Composing traits with nested classes allows us to merge arbitrarily complex units of code. In other languages this kind of flexibility requires complex patterns like dependency injection, as shown below:

As you can see, we can define more code using «» while only repeating the needed dependencies. We will use this idea in the following, more elaborated scenario: Bob and Alice are making a video game. In particular, Alice is doing the code related to loading the game map from a file.
As you can see from the non modularized code above, Alice code is tightly connected with Bob code: She have to instantiate «» and all the kinds of «»s. In a language like Java, Alice would need to write her code after Bob have finished writing his, or they would have to agree to use dependency injection and all the related indirections. Instead, in 42 they could simply factorize their code into two independent traits:
Now that the code of Alice and Bob are separated, they can test their code in isolation:
 %weight")
    }
  Wall = /*..*/
  Map = {
    var S info
    class method mut This (S info)
    class method mut This empty() = \(S"")
    mut method Void set(Item i) = this.info(\info++i.info()++S.nl())
    }
  }
TestAlice = (
  files=FS.#$()
  {}:Test"justARock"(
    actual=MockAlice.load(files=files, fileName=S"justARock.txt")
    expected=S"""
      |Rock: Point(5,6) -> 35
      """)
  {}:Test"rockAndWall"(
    actual=MockAlice.load(files=files, fileName=S"rockAndWall.txt")
        expected=S"""
      |Rock: Point(x=5, y=6) -> 35
      |Wall: Point(x=1, y=2) -> 10
      """)
  ..//more tests here
  )
]]>

(4/5)Typing considerations

Object oriented programs often contain entangled and circular type definitions. For example, strings «» have methods «» and «», while both «» and «» offer a «» method. That is, while circular values are a double edged sword (useful but dangerous), circular/recursive types are unavoidable even in simple programs. So, how do recursive types interact with metaprogramming? Path names can only be used in execution when the corresponding nested class is fully typed, thus the following example code would not work:

We can not start computing «» since «» depends from «», that is defined later. Swapping lines would not help, since «», in turn, depends from «». Later we will learn how to overcome this issues and still generate code with the intended structure. As shown below, library literals can instead be manipulated even if they are not fully typed.
Here «» can be computed even if «» is still unavailable. However, such a manipulation must happen in place: we can not use traits to reuse untyped code; that is, the following would not work:
«» can not be used before «» is defined.

This also allows us to avoid defining many redundant abstract methods. Consider the following working code:

This code is allowed even if «» does not contain an abstract definition for «». This feature is often used by 42 programmers without even recognizing it, but it is brittle: when method calls are chained (as in «») or when binary operators or type inference are involved, the system needs to be able to guess the return type of those missing methods.

(5/5)Metaprogramming summary

Here we have introduced the way that 42 handles metaprogramming and code reuse. We focused on «» and «». Composing code with «» or «» we can partition our code-base in any way we need, enabling simple and natural testing and code reuse patterns.
When reusing code, we have to be mindful of missing types and missing methods. Structuring the code-base to avoid those issues will require some experience, and is indeed one of the hardest parts about writing complex 42 programs.

      Previous ... Next