Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

That's something I have been pondering for some time.

I believe it's a false dichotomy.

My thought is still that structural supersedes nominal.

A nominal interface is just another constraint added to the list of constraints of an underlying structural interface?



In a nominal type system, a method x() is part of the interface X, while in a structural one it's part of the implementor of said interface. In Go there's a Human.HasOrgan(), not an AbstractBody.HasOrgan().

A consequence of this is that in Rust, which has a nominal system, you can implement two traits that contain a method with the same name and are required to disambiguate at the call site. In Go you can't do that, since the method is part of the concrete type.


Fair. That's not really in contradiction too.

The additional naming constraint added to a structural interface would form a sort of namespace for methods.

I think in the comments below that someone likens this to tags in C++.


A nominal type system is not a more constrained version of a structural one. That statement would imply that any program written for the former would work using the latter as well, which is false. Name collisions would simply not resolve.

For it to work, you need to add a namespace to all the colliding methods (a simple one would be a prefix like people do in C).

A nominal system is a more constrained structural system in some ways, but the opposite is true as well, so it's not as simple as 'nominal is subset of structural'.


Hmmh. You seem to be restating what was said above.

A nominal type system still is superseded by a structural type system.

The difference is in how a type is defined. Or what kind of constraints are in entailment said otherwise.

An interface enforces constraints. The difference here is merely that the current implementations only have either one of these type of interfaces. So for the structural type system, all methods are in the global namespace, somehow.

That's all. Because our current languages are this way doesn't mean that the two concepts cannot be reconciliated or that one is just better than the other.


Yeah I think our arguments overlap in some ways.

> That's all. Because our current languages are this way doesn't mean that the two concepts cannot be reconciliated or that one is just better than the other.

I don't think I agree with this though, I believe they're fundamentally different. The whole point of structural constraints is that they don't need the type to be aware of them. The point of nominal constraints though is that they require the type to explicitly acknowledge them.

In an ideal situation, everyone names and types things the same ('logical') way, so structural constraints 'just work'. A type implements has_organ, and an interface requires has_organ, and the type is automatically compatible with the interface.

A nominal system is the opposite though; the type explicitly understands what a specific interface implies and formally states it.

I just can't see how there's a subset-superset relationship, or how they can somehow be reconciled.


One way to see it is that a type has a given methods located in a given namespace in the nominal type system.

A nominal type system doesn't necessarily enforce semantics either.

It just enforces the location of a method definition.

Seen that way, because the relation is dual, one could indeed claim that a structural interface is a nominal interface where the name constraint is elided.

But just as in subtyping, one less constraint also means bigger set.

Of course if one were to decide that an object satisfying a nominal interface doesn't satisfy the structural interface obtained by ignoring the namespace, then I'd agree as well, these concepts would be disjoint.

I don't think they are though but I don't know of a language that ever mixed both either.


AFAIK Python [optional] type system supports both. The nominal types are the "common" types, while the protocols [1] are structural. It's quite cool, actually :)

[1] https://peps.python.org/pep-0544/


Nominal interfaces can still be useful though, as they convey a stronger sense of intent than structural. For example, java.io.Serializable is a completely empty interface that classes “implement” to signal that they are safe to serialize. As a structural interface, it’d be useless.


structurally you can do something similar by adding some tag (in the form of a constant or nested type) to your class. For example:

  template<class T> concept serializable = requires { typename T::is_serializable_tag; };

  void serialize(serializable auto x) {...};

  struct NotSerializableClass { ... };
  struct SerializableClass { using is_serializable_tag = void; ... };

  serialize(NotSerializableClass{}); // error
  serialize(SerializableClass{}); // all good
In C++, specializing a trait is also an option. So, while nominal and structural interfaces are not the same, sometimes the lines are blurred.


Well it depends on a runtime/compile-time distinction. A nominal type is a structural type with a compile-time constraint.

If you have compile-time only constants you can model nominals with structural,

    type Square
        static const IsSquare = true
        
        var length = 10
You can kinda hack-in subtyping,

    type Shape
        static const Shape = true

    type Square
        import static from Shape
        static const IsSquare = true
        
        var length = 10


This is routinely done in c++ with tags (for example iterator_tag). Tags inheritance is also a thing.


In practice C++ Concepts don't do what you're suggesting.

The C++ 20 Standard Library provides numerous concepts which have a very different semantic requirement than the syntax they're checking. If you violate the syntactic requirement of course that'll earn you a compiler error, but if you violate the semantic requirements that's silently an ill-formed C++ program, it has no meaning whatsoever and might do absolutely anything if run.

If these were nominal, we could say, well, nobody should have deliberately implemented this inappropriate Concept, similar to an unsafe Rust trait, the act of implementation is a promise to others. But C++ Concepts aren't nominal and so there was no opportunity to do that and so in practice such deviations are likely very common despite the potentially drastic consequences.


I have been programming in C++ for almost 20 years [1] and I don't remember ever being bitten by accidental concept conformance. So I object to the "likely very common" description. Implicit conformance was very much an explicit design goal.

[1] yes, concepts as an explicit language feature are new, but C++ has had de-facto concepts since Stepanov work on the original STL in the 90s.


Since I know better than to suggests C++ programmers might be more capable of making mistakes than they realise, lets try a different question: How do you spot this mistake when reviewing other people's code? Do you memorise a list of all the semantic requirements of each concept so that you can mentally check that the concept's requirements are satisfied appropriately by what was written each time ?


This isn't something I look for in code reviews because it's just not something I've ever see be the source of a bug. There are a million bugs that I've eventually tracked down to some subtle C++ thing, but I've never had one come down to a type which appears to conform to one of the standard library's concepts but actually doesn't.


I expect other people to write tests (including compile time tests).


If you were worried about behavioural problems, including UB, tests would help.

But alas the problem here is IFNDR [Ill-formed No Diagnostic Required] so the compiler can't help you. All semantic constraints are your problem as the programmer, C++ decided that it's not the compiler's concern whether the program meets semantic constraints. Testing doesn't necessarily help at all, which is probably surprising.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: