Hacker News new | past | comments | ask | show | jobs | submit login

From https://luau-lang.org/typecheck

> Luau’s type system is structural by default

I think that makes it the second (production-ready) language to have a primarily-structural type system, after TypeScript/Flow(/and Closure to some extent) for JS. I wonder if it will follow in their impressive footsteps or if it will tread new territory.

One of the things that Flow gets right that TypeScript doesn't yet support is switching to nominal type-checking between class instances. In TypeScript you can assign an instance of one class to an instance of another as long as they have compatible fields. Compare:

Flow: https://bit.ly/3jZx4rB

TS: https://tsplay.dev/mZaL1N

TypeScript doesn't have any understanding of prototypes or prototypal inheritance, which is a big gap considering that it's a fundamental aspect of JS.

I'm really hoping Luau will come to do the right thing with tables and prototypes. Perhaps it already does, but unfortunately there doesn't seem to be a Luau playground to try in the browser.




Luau already has some notion of nominal types to deal with the fact that the Roblox API is basically a ton of C++ classes that have been mapped over via the FFI. Some of these types are totally structurally identical and yet incompatible.

We'd like to afford the same kind of ideas for native Luau code, but we're not there yet.


Super happy to see this released, and also for all the kids getting started programming on Roblox learning that there is a better way than random tag-checked mush :-) Good on you all!


Pony, amusingly, has both structural and nominal typing. https://tutorial.ponylang.io/types/traits-and-interfaces.htm...


So has COBOL, sort-of.

A regular MOVE is sort-of structural. It moves field i of the source to field i of the destination. Source and destination need not be of the same type, though.

MOVE CORRESPONDING is nominal. It moves field Foo of the source to field Foo of the destination (leaving fields that do not have corresponding items alone)

In both cases, data may be converted, for example from alphabetic to alphanumeric.


> a big gap considering that it's a fundamental aspect of JS.

To be fair to Typescript, it's an insane fundamental aspect of JS.


Funny that you would mention that in a thread on a statically-typed language based on Lua. Don't you need some insane mechanism to be flexible at runtime anyways? Either it will be reflection, or something like prototypes (in OO languages).


What about Go and OCaml? And I'm sure there are others.


I think OCaml isn't just structural. For example, sum types (variants in OCaml) have a nominal form which is the default: https://ocaml.org/manual/coreexamples.html#s%3Atut-recvarian..., but also a structural form, polymorphic variants: https://ocaml.org/manual/polyvariant.html. I think it's the same for records, I didn't find a way to express the type "a record that has a certain field" as an argument of a function. You can do this with objects and modules I think, but not with records.


Structural records are in fact "object types" in ocaml, which you can get the shell to tell you by calling a random method name.

  # let f x : int = x#foo;;
  val f : < foo : int; .. > -> int = <fun>
Actually there is not really a type of "a record that has at least some fields":

  # type t = < foo : int; .. >;;
  Error: A type variable is unbound in this type declaration.
  In type < foo : int; .. > as 'a the variable 'a is unbound
Its hidden (row) polymorphism, it's not subtyping but type abstraction. We're implicitely quantifying over the actual type:

  # type 'a t = < foo : int; .. > as 'a;;
  type 'a t = 'a constraint 'a = < foo : int; .. >
For the record (ha!), for people who don't know ocaml here are "structural sum types", or polymorphic variants as we call them.

  # let x = `Bar;;
  val x : [> `Bar ] = `Bar
above: type of `Bar is a sum containing "at least" (polymorphic) `Bar

  # type int_opt = [ `Some of int | `None ];;
  type int_opt = [ `None | `Some of int ]
above: some "exact" nameless sum type, with arguments

  # type 'a t = [< `Foo | `Bar ] as 'a;;
  type 'a t = 'a constraint 'a = [< `Bar | `Foo ]
above: polymorphic type of sums which contain at most these two constructors.

refs:

https://dev.realworldocaml.org/objects.html

https://dev.realworldocaml.org/variants.html#polymorphic-var...


I forgot about OCaml (since I normally associate it with the more sophisticated typing stuff). I don't know if I'd completely count Go since interfaces only apply to methods, not fields


Disclaimer: Not a regular Go user.

I was thinking of how you can embed types in structs. You automatically dereference the fields and methods of the components directly from the outside.


Yep, in go the inheritance (using the term loosely) structure can apply to interfaces (which specify groups of functions) and structs (which specify groups of data) and roughly does the same thing in both instances.


> In TypeScript you can assign an instance of one class to an instance of another as long as they have compatible fields.

Fun fact: If you add a private field to a class it'll behave nominally in TS. This is because private fields kinda require nominal relations to function. So, in a way, it does support "switching" to nominal type checking for classes - the opt in is simply per-class.


It won't do variance [0] right, will it?

[0] https://flow.org/en/docs/lang/variance/


I think variance can be simulated by typing the private field as an existing covariant/contravariant/invariant type

  class CovariantFoo<T> {
    private phantom!: T
  }

  class ContravariantFoo<T> {
    private phantom!: (_: T) => void
  }

  class InvariantFoo<T> {
    private phantom!: (_: T) => T
  }
(inspired by Rust [0])

[0] https://doc.rust-lang.org/nomicon/phantom-data.html


I believe this is conceptually similar to how "brands" can operate to add HKTs to TypeScript/Flow

https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-ki...

https://cs.emis.de/LIPIcs/volltexte/2015/5231/pdf/21_.pdf


But variance depends on position where the value is used, not at the time of declaration, superclass/subclass at parameter/return value position can't be correctly encoded like this, can it?


I think most languages define variance at type definition level, notable exceptions being Kotlin which supports both [0][1] and Flow. But yeah, TS doesn't support (variable-)declaration-site variance which I didn't realize you were asking in my previous answer.

[0] https://kotlinlang.org/docs/generics.html#variance

[1] https://kotlinlang.org/docs/generics.html#declaration-site-v...


Doesn't OCaml support it as well?


I'm less familiar with OCaml and don't know off the top of my head. Doing a quick search I was only able to find references to type declaration variance [0], though I learned that Java also supports use-site variance too [1]

[0] https://blog.janestreet.com/a-and-a/

[1] https://blog.jooq.org/tag/use-site-variance/


Oh cool, hadn't realized this. Also thanks for your great work! I'm rooting for some of your exploratory PRs!


> I think that makes it the second (production-ready) language to have a primarily-structural type system

I assume we are talking about a static type system here? Many common "scripting" languages are structurally typed - what Python calls duck typing.




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

Search: