In Jsonnet, a module is typically a Jsonnet file that defines an object whose fields contain useful values, such as functions or objects that can specialized for a particular purpose via extension. Using an object at the top level of the module allows adding other fields later on, without having to alter user code. When writing such a module, it is advisable to expose only the interface to the module, and not its implementation. This is called encapsulation, and it allows changing the implementation later, despite the module being imported by many other Jsonnet files.
Jsonnet's primary feature for encapsulation is the local
keyword. This makes it possible to define variables that are visible only to the module, and impossible to access from outside. The following is a simple example. Other code can import util.jsonnet but will not be able to see the internal
object, and therefore not the function square
.
// util.jsonnet
local internal = { square(x):: x*x,
};
{ euclidianDistance(x1, y1, x2, y2):: std.sqrt(internal.square(x2-x1) + internal.square(y2-y1)),
}
It is also possible to store square
in a field, which exposes it to those importing the module:
// util2.jsonnet { square(x):: x*x, euclidianDistance(x1, y1, x2, y2):: std.sqrt(self.square(x2-x1) + self.square(y2-y1)), }
This allows users to redefine the square function, as shown in the very strange code below. In some cases, this is actually what you want and is very useful. But it does make it harder to maintain backwards compatibility. For example, if you later change the implementation of euclidianDistance
to inline the square call, then user code will behave differently.
// myfile.jsonnet local util2 = import "util2.jsonnet" { square(x):: x*x*x }; { crazy: util2.euclidianDistance(1,2,3,4) }
In conclusion, Jsonnet allows you to either expose these details or hide them. So choose wisely, and know that everything you expose will potentially be used in ways that you didn't expect. Keep your interface small if possible.
A common belief is that languages should make local
the default state, with an explicit construct to allow outside access. This ensures that things are not accidentally (or apathetically) exposed. In the case of Jsonnet, backwards compatibility with JSON prohibits that design since in JSON everything is a visible field.