Nominal for Storing, Structural for Manipulating

46 pointsposted 3 days ago
by todsacerdoti

15 Comments

deredede

3 days ago

> What this means is that all nominal variants and records are really just nominal wrappers over structural variants or records.

If you have both nominal types and structural types in your language, you can already do this, while keeping the ability to be nominal only or structural only when you want.

In the following OCaml code variant inference in pattern matching is not automatic (although the compiler will warn you about the missing cases, which helps you figuring what to write), but the types do get refined in the corresponding branch.

    type 'a tree =
      Tree of
        [ `Empty
        | `Branch of ('a tree * 'a * 'a tree)
          (* Unfortunately, inline records are not supported here,
             so you have to use tuples, objects, or a mutually recursive record
             type. *)
        ]
      [@@unboxed]

    (* You can give aliases to subsets of constructors *)
    type 'a branch =
      [ `Branch of ('a tree * 'a * 'a tree) ]
    
    let f (x : 'a branch) = ()
    
    let f x =
      match x with
      | Tree `Empty -> ()
      (* You need to be explicit about the cases you want to handle. This pattern
         could also be written `Tree (#branch as rest)`. *)
      | Tree (`Branch _ as rest) -> f rest
The one feature I'd really like in this space would be the ability to refer to a subset of the constructors of a nominal types as if it was a (restricted) polymorphic variant that could only be recombined with another subset of constructors of the same nominal type. It would allow some of the power of polymorphic variants without losing the efficient representation allowed by nominal types knowing the possible variants ahead of time.

js8

3 days ago

The nominal vs structural distinction (in both programming and math) goes beyond types. Should names in programs matter?

Consider two functions, say copySurname and copyStreetName. They do same exact thing, but in different context. No sane person would call copySurname for the street, even though the function is the same.

So there is this tension, should name of the function (or of the type) only reflect it's internal structure (structuralism), or should it also reflect the intended domain application (nominalism)?

There is a formalism school of mathematics that is pretty much the hardcore structuralist, i.e. names of the objects don't matter, only their relationships. But most mathematicians (and all programmers) reject that view.

sevensor

3 days ago

Although I have my frustrations with Python’s bolted-on type checking, particularly how clunky the type annotations can be at runtime (this type is just the string “Foo | Bar | int”, good luck!), I think the pragmatic approach is working out pretty well on the nominal versus structural front. I.e. I can make it an error to assign a number in radians to a variable of type meters (NewType), but I can also construct a dictionary of the right shape and have it satisfy a TypedDict annotation. Or I can write a Protocol, which is the biggest hammer in the type system.

ojkelly

3 days ago

The name should represent what the function does, it should indicate its purpose.

The distinction is useful even when it’s structurally identical to another function.

Two identical functions in different contexts or domains often diverge. When they’re not arbitrarily bound by the fact they have the same contents it’s easy to extend one and not the other.

During compilation, they could both end up using the same actual implementation (though I’m not sure if any compiler does this).

bheadmaster

3 days ago

In real-world software writing, there's a huge pull towards nominalism (i.e. naming code parts by their domain purpose), but in my experience, the most useful way is to have structural names for internal mechanisms, then use their interfaces in nominal way by describing their role in the business logic.

That way, all nominalism is simply a descriptive wrapper over structuralism.

PaulHoule

3 days ago

I think is forgotten here that one of the benefits of nominal typing is that the compiler can know that data layout at run time so performance benefits.

There has been so much ink spilled on the question of what kind of type systems help programmers be productive but there is not such controversy on the performance side.

k_g_b_

3 days ago

I think you're confusing nominal with static and structural with dynamic. This might be because there's few instances of more powerful static structural types implemented in common programming languages - OCaml's polymorphic variants, Typescript (partially dynamic). Very common structural types available in many statically typed programming languages are tuples.

The compiler can still know and optimize the data layout of any static structural type and tuples certainly are optimized in e.g. Rust. However, the flexibility of other structural types like polymorphic variants or set-theoretic types like unions mean that the data layout also needs to be more flexible and that comes with some overhead e.g. vs a nominal sum type (like in ML, Rust enums, etc) or struct/record.

Missing data layout optimizations comes mostly due to necessary uniform representation of dynamic types - the same as for nominal types - e.g. through runtime polymorphism language features as OCaml has by default, virtual methods or Rust's trait objects. Whether a structural type system allows such dynamic features (e.g. OCaml's row polymorphism) or not is a design question - I think there's still plenty of use for structural types in a language with only compile time polymorphism (monomorphization).

See also deredede's comment https://news.ycombinator.com/item?id=42191956

agentultra

3 days ago

Do you mean at compile time?

I’m mostly familiar with Haskell which does type erasure. I think the means that there is no type checking at run time.

I think dependently typed languages would the benefit of knowing structure at compile time enough to detect things like dead branches and impossible cases which can optimize by removing code. I’m not sure that all types are erased in such languages though.

lmm

3 days ago

Premature optimisation is the root of all evil. If you get the semantics right then good performance will generally follow, if you make the wrong thing then it doesn't matter how fast it is.

vrotaru

3 days ago

I guess Modula-3 was doing it as well.

Records were structurally typed. But you can "braid"(?) a record and that will make it nominal type.

readthenotes1

3 days ago

An odd to see Java thrown in there without methods...

user

3 days ago

[deleted]

molikto

3 days ago

convertTree doesn't work because Tree uses Tree type recursively.

jerf

3 days ago

Since the author is writing their own language, their language can fix that; it's not hard to figure out how to implement that.

What I find horrifying is that types are most useful when they are more than just a shape. Just because two things have a type X[A] {X[A], A, X[A]} doesn't mean it's the same binary tree. A trivial example is that one tree could be sorting by the "id" field and another could be sorting on a "username" field, and simply casting one to the other means you've now got a garbage data structure and none of the functions/methods operating on it are going to return good data now. It's a bit of a culture shock to see such Haskell-esque type notation combined with excitement over blowing away everything about a type other than its brute shape.