Ideal notation allows developer to specify the type precisely yet without boilerplate code, using parameterized types (generics) as well as type flavors. Like generics, type flavors define a family of related types. Unlike generics, the family is finite; there are currently six type flavors: readonly, writeonly, mutable, immutable, deeply_immutable, mutable, any. The type flavor prefixes the typename; it is possible to combine both type flavors and type parameters, with a declaration such as readonly list[mutable value].
The readonly flavor is equivalent to const in C and C++: it specifies that the only legal operations on the entity are the ones that don’t change its state. In the list example, you can access the elements but not set them—although some other code may change the list state. The immutable flavor is a stronger contract: it specifies that the state of the entity is guaranteed to never change. In the list case, the state of the list is frozen, but you can modify the state of the elements of the list. The deeply_immutable is a stronger contract still: it specifies that the state of the value is transitively immutable. The deeply_immutable list is used to specify that neither the list nor any of the entities that it references, directly and indirectly, will change its state.
The writeonly flavor specifies that the only legal operations are the ones that write to the entity. The writeonly list supports append() and insert() but not get(). The mutable flavor supports all the operations; it is the default flavor, and the modifier can be omitted. The any flavor is useful when either readonly or writeonly or any other flavor will do.
For a given type, the supertyping relationship of its flavors is as follows:
any ^ ^ / \ / \ readonly writeonly ^ ^ ^ / \ / / \ / immutable mutable ^ / deeply_immutable
When methods are defined, they can have type flavor as a suffix. In this case, the method applies to the appropriately flavored type. (Prefix pure specifies a pure function maps to readonly type flavor.) Consider the declaration:
interface list[element] element get(integer index) readonly; void append(element the_element) writeonly;
It is equivalent to the following six declarations:
interface any_list[element]; interface readonly_list[element] extends any_list[element]; element get(integer index); interface writeonly_list[element] extends any_list[element]; void append(element the_element); interface mutable_list[element] extends readonly_list[element], writeonly_list[element]; interface immutable_list[element] extends readonly_list[element]; interface deeply_immutable_list[element] extends immutable_list[element];
The introduction of type flavors makes practical the strict adherence to Liskov substitution principle. This is illustrated by elegant solutions to two challenges: the circle-ellipse problem and the variance in collection types.
In the case of the circle-ellipse problem, the challenge is how circle can handle mutatition operations from the ellipse that might make axis unequal. The ideal solution is simple: circle, instead of inheriting from ellipse, should inherit from readonly ellipse. Then it should support the fetching of width and height, but not the mutation that might turn a circle into an an ellipse. (The code for this example can be found in showcase/circle.i; make circle runs it.)
With the variance in collection types, the challenge is to define a subtyping relationship on generic collection types such as list. Given that string is an object, is list[string] a list[object]? That depends on the flavor of the list:
- readonly list[string] is a readonly list[object]
- writeonly list[object] is a writeonly list[string]
- mutable list[string] is not a mutable list[object], nor the other way around
This is a common pattern, and ideal type system supports it directly: by adding the combivariant modifier to the type parameter (as in interface list[combivariant value element]) all the subtyping relationships among different flavors are setup. It is also possible to use covariant and contravariant modifiers, similar to in and out modifiers in C#. (In the current implementation, variance annotations are not fully supported.)