Backward Compatible Refactoring

Nirum is designed to marshal well-typed data for networking. That means types defined in Nirum IDL are shared with other programs over the network. “Other programs” mean they usually aren’t deployed together. Even if some of your types defined in Nirum IDL was evolved and the changes were deployed to some programs that use them, some other programs still works as the types were not changed yet until the changes are deployed to all involved programs together.

To help types defined in Nirum IDL to be possible to be evolved with backward compatibility, Nirum types provide several properties.

Behind names for old names

The most of identifiers in Nirum IDL have a facial name and a behind name. By default, these two names are the same. For example, the following two declarations are equivalent:

record money (decimal amount, currency currency);

record money/money (decimal amount/amount, currency currency/currency);

A facial name is a descriptive identifier for humans. Object codes generated by Nirum compiler have identifiers named after facial names.

On the other hand, a behind name is an arbitrary identifier for programs. JSON payloads produced by serializers (that are generated by Nirum compiler) have identifiers named after behind names.

The following declaration shows a record and its fields having behind names different from their facial names:

record point2d/point (
    float64 left/x,
    float64 top/y,
);

The above IDL becomes to the following Python code for example (although it approximates details):

class Point2d:
    def __init__(self, left: float, top: float) -> None:
        self.left = left
        self.top = top

For example, Point2d(left=1.23, top=4.56) value is serialized to the following JSON payload:

{
    "_type": "point",
    "x": 1.23,
    "y": 4.56
}

Note that the above JSON payload names its type and fields after the behind names like point, x and y, whereas the above Python code names its class and attributes after the facial names like Point2d, left and top.

Separating a facial name from a behind name is useful when to rename a misleading name (or an outdated name) to a more clear name (or an up-to-date name).

Type alias

Type aliases are not distinct. While it’s being compiled all type aliases are simply replaced by types they refer to. So every type alias doesn’t break backward compatibility. Feel free to define type aliases!

Unboxed type

Unboxed types can be used for semi-refactoring: you can make people to say things by a specific name in program codes, while leave programs to communicate each other in the way they’ve done.

The key difference between unboxed types and single-field records is that unboxed types don’t make additional structure on its inner type whereas records always adds object structure to wrap its fields.

For example, the following two types are seemingly equivalent to program codes and programmers:

unboxed meter (bigint);

record meter (bigint value);

However, they make different JSON payloads:

"123"
{"_type": "meter", "value": "123"}

This means unboxed types are indistinguishable from their inner types in JSON payloads, while in program codes they are distinct individual types.

Suppose we’ve represented distance bigint with assuming 1 means 1 meter.

service map-service (
    bigint find-distance(coord a, coord b),
);

As such scale/unit assumptions are error-prone, we want to encode them as distinct type without breaking backward compatibility. It’s the when unboxed types are useful:

unboxed meter (bigint);

service map-service (
    meter find-distance (coord a, coord b),
);

The above change doesn’t affect to JSON payloads the find-distance method returns, but in program codes we become able to deal with distance using meter type rather than primitive bigint type.

Removing a field

Any fields in payload that are unlisted in an interface definition are ignored by a deserializer. If you are going to remove an existing field you should deploy the newer version to a payload consumer first, and then a payload provider last.

Interchangeability of enum type and text

Enum types are represented as JSON strings as like text. If a field had been represented as text but there are only the limited number of values, it’s okay to be refactored to enum type.

Suppose the following record had represented gender as freeform text:

record person (
    text name,
    text gender,  // represented as texts e.g. "male", "female"
);

Although it had become text due to a design mistake, we still have a chance to make up for it. Because there are only few cases:

{"_type": "person", "name": "Jane Doe", "gender": "male"}
{"_type": "person", "name": "John Doe", "gender": "female"}

The following change is still mostly compatible with the previous revision:

enum gender = male | female | unknown;

record person (
    text name,
    gender gender,
);

Making field optional

An option type modifier (?) indicates a field can be null in JSON payloads. Fortunately making a field which had disallowed null to allow null does not require any structural changes of JSON payloads. In other words, every valid JSON payload that a field disallowing null accepts is also accepted by the same field but of null allowance.

On the other hand, when it comes to reversed, optional fields cannot be required without any compatible breakage. Also, if two programs communicate each other, one deserializing and receiving payloads has to be deployed before other one serializing and sending payloads.

Interchangeability of set and list

A set contains zero or more unique values of the same type, without order.

A list contains zero or more values of the same type, in an order, and allows duplicated values.

However, both set and list types are serialized to equally an array in JSON payloads. So a set field and a list type are interchangeable each other unless their element types are different.

(More accurately, a set and a list still might be interchangeable even if their element types are different since two element types may be interchangeable. For example, {[a]} and [{a}] are interchangeable since [a] and {a} are interchangeable, and a and a are interchangeable again.)

When a JSON array serialized from a set field is deserialized to a list, its order becomes arbitrary. A program accepting payloads have to deal with its lack of order if necessary.

When a JSON array serialized from a list field is deserialized to a set, the same values shown more than once are collapsed to unique values. Also, its order is not preserved.

Interchangeability of union type and record type

Sometimes we need to evolve an existing record type to be extended.

But when we change a record type to a union type, it breaks backward compatibility. Suppose we have a record type named name that looks like:

record name (text fullname);

An example of JSON serialized one would look like:

{
    "_type": "name",
    "fullname": "John Doe"
}

What if we need to be more sensible to culture-specific names? Now we decide to change it to a union:

union name
    = wastern-name (text first-name, text? middle-name, text last-name)
    | east-asian-name (text family-name, text given-name)
    | culture-agnostice-name (text fullname)
    ;

Since union types requires "_tag" field besides "_type" field when they are deserialized, data sent from the older programs becomes to break compatibility.

In order to make union types possible to deserialize existing record data (which lacks "_tag" field), we need to choose default tag for data lacking "_tag" field:

union name
    = wastern-name (text first-name, text? middle-name, text last-name)
    | east-asian-name (text family-name, text given-name)
    | default culture-agnostice-name (text fullname)
    ;

With a default tag, union types become possible to deserialize data lacking "_tag" field, and they are treated as an instance of the default tag. For example, where we have a payload data like:

{
    "_type": "name",
    "fullname": "John Doe"
}

It’s treated as equivalent to the following one:

{
    "_type": "name",
    "_tag": "culture_agnostic_name",
    "fullname": "John Doe"
}