Since most popular languages expose nominal typing or duck typing, it's worth to talk about the two concepts and their uses.
Nominal typing
Nominal typing is about naming your concepts and being able to refer to them uniquely. Languages like C, C++, C#, Java, Rust and Swift are nominally typed. For example, if you want to define an interface that users can implement, you give the interface a name, and document what concept it encapsulates:
// Implementing this interface means that your type is serializable to a text format.
interface ISerializable
{
public void Serialize(StringBuilder sb);
}
When you implement this interface for your type, you know exactly what concept it encapsulates. When you write a type, it is not enough to simply implement the members of the interface, you have to explicitly state that you implement it. For example, the following will not compile, because the interface implementation is not stated explicitly:
class Person
{
public string Name { get; init; }
public int Age { get; init; }
// This is not ISerializable.Serialize, because the class does not implement ISerializable
public void Serialize(StringBuilder sb) =>
sb.AppendLine($"Name: {Name}, Age: {Age}");
}
static void SerializeToFile(ISerializable s) { ... }
SerializeToFile(new Person() { ... }); // ERROR: Person does not implement ISerializable
The key takeaway is that in nominal typing you attach concepts to names/entities and not to the shape/implemented methods of a type.
Structural typing
Structural typing on the other hand does not care about explicitly stated concepts, it cares about how types look, what fields or methods they have. This is similar to C++ templates, where you do not state constraints for template variables, the substitution simply fails, if a type can not be used for a template:
// No explicit constraints on the type to ensure '+' exists for two types of T
template <typename T>
T add(T x, T y) {
return x + y;
}
add(1, 2); // Ok, substituting integers into 'add' is fine
add(true, "hello"); // Error, substitution causes an error, no proper overload found
This means that constraints on types can mostly be expressed as a set of required fields and methods we expect from a type. For example, if we want to expect a generic type in Haxe to be hashable, we can write something like:
function compareHashes<T: { function hashCode():Int; }>(a:T, b:T):Bool {
retrun a.hashCode() == b.hashCode();
}
This does not mean that we can't name structural components, we can of course alias them. But the important thing is that for nominal typing, two different interfaces with the same set of methods will still mean different things, for structural typing two different aliases with the same set of constraints will be perfectly equivalent.
Notable examples
Languages like C# and Java use nominal typing, but the shapes proposal essentially wants to introduce structural typing for generics in C#.
C++ is nominally typed, except for templates, where it can do duck typing (unconstrained) and structural typing (SFINAE, concepts).
Go is primarily nominally typed, but implementing an interface is implicit, if a type implements all methods for an interface, it is considered implemented.
Haxe is primarily nominally typed, except for anonymous structures, which use structural typing.
Thoughts for the new language
To me it looks like most languages prefer primarily nominal typing. Its advantage is also it's disadvantage: You have to explicitly implement the concept for your type. This ensures that it's not just an accidental signature match, but an explicit statement that you do indeed implement the contract. This can be a major disadvantage in languages like C#, where you can't implement an interface externally for a type.
Some languages like C# turn to structural typing because constraint-based generics can be limiting, if there are not enough constraints to express the requirements. Languages like Rust allow for external trait implementations and their generic constraints are more sophisticated, allowing to require the existence of operators and such.
I'd say that a primarily nominal typing system could be desirable as long as we don't fall into the traps mentioned above. Allowing external trait/interface implementations could already improve a lot. Even if we do allow structural typing, I really wouldn't want to have a separate concept for that. I'd either have something like Haxe anonymous structures or allow to mark an interface constraint to mean structural equality, something like:
static void SerializeToFile<T>(T s)
where T : structural ISerializable
{
...
}
Edit: One thing I forgot to mention in favor of nominal typing is that it can resolve name collisions between different interfaces. If two interfaces define a member with the same signature - which can be surprisingly common -, nominal typing can give a tool to disambiguate them. In languages like C# you can just write an explicit implementation, in Rust you can just use a fully qualified syntax on invocation. For structural typing you'd require very specific names, which can be tiring, verbose and still not avoid the problem 100%.
Introduction
Since most popular languages expose nominal typing or duck typing, it's worth to talk about the two concepts and their uses.
Nominal typing
Nominal typing is about naming your concepts and being able to refer to them uniquely. Languages like C, C++, C#, Java, Rust and Swift are nominally typed. For example, if you want to define an interface that users can implement, you give the interface a name, and document what concept it encapsulates:
When you implement this interface for your type, you know exactly what concept it encapsulates. When you write a type, it is not enough to simply implement the members of the interface, you have to explicitly state that you implement it. For example, the following will not compile, because the interface implementation is not stated explicitly:
The key takeaway is that in nominal typing you attach concepts to names/entities and not to the shape/implemented methods of a type.
Structural typing
Structural typing on the other hand does not care about explicitly stated concepts, it cares about how types look, what fields or methods they have. This is similar to C++ templates, where you do not state constraints for template variables, the substitution simply fails, if a type can not be used for a template:
This means that constraints on types can mostly be expressed as a set of required fields and methods we expect from a type. For example, if we want to expect a generic type in Haxe to be hashable, we can write something like:
This does not mean that we can't name structural components, we can of course alias them. But the important thing is that for nominal typing, two different interfaces with the same set of methods will still mean different things, for structural typing two different aliases with the same set of constraints will be perfectly equivalent.
Notable examples
Thoughts for the new language
To me it looks like most languages prefer primarily nominal typing. Its advantage is also it's disadvantage: You have to explicitly implement the concept for your type. This ensures that it's not just an accidental signature match, but an explicit statement that you do indeed implement the contract. This can be a major disadvantage in languages like C#, where you can't implement an interface externally for a type.
Some languages like C# turn to structural typing because constraint-based generics can be limiting, if there are not enough constraints to express the requirements. Languages like Rust allow for external trait implementations and their generic constraints are more sophisticated, allowing to require the existence of operators and such.
I'd say that a primarily nominal typing system could be desirable as long as we don't fall into the traps mentioned above. Allowing external trait/interface implementations could already improve a lot. Even if we do allow structural typing, I really wouldn't want to have a separate concept for that. I'd either have something like Haxe anonymous structures or allow to mark an interface constraint to mean structural equality, something like:
Edit: One thing I forgot to mention in favor of nominal typing is that it can resolve name collisions between different interfaces. If two interfaces define a member with the same signature - which can be surprisingly common -, nominal typing can give a tool to disambiguate them. In languages like C# you can just write an explicit implementation, in Rust you can just use a fully qualified syntax on invocation. For structural typing you'd require very specific names, which can be tiring, verbose and still not avoid the problem 100%.