5 ms·
Hmm, IME the preferred type systems are structural - a function shouldn't care what the name is of the struct passed to it, it should just work if it has the co
by b_e_n_t_o_n 1y ago
Hmm, IME the preferred type systems are structural - a function shouldn't care what the name is of the struct passed to it, it should just work if it has the correct fields.
- stirfish 1y ago> should just work if it has the correct fields. Correct fields by...name? By structure? I'm trying to understand.
- b_e_n_t_o_n 1y agoBy name, type, and structure. In typescript for example: let full_name = (in: { first: string, last: string }) => in.first + " " + in.last Then you can use this function on any data type that satisfies that signature, regardless of if it's User, Dog, Manager etc.
- tsimionescu 1y agoThis is very much backwards from most programming practices. It's much more common to have an operation that makes sense for many structurally different types, such as add(a, b) where a and b could be integers, floating point numbers, complex numbers, 3D vectors, 4D vectors, matrices etc.
- b_e_n_t_o_n 1y agoMost programming happens in nominally typed languages where the above isn't trivially possible. To be fair that example is contrived, but you can imagine operations happening on large dicts where only a small subset of the fields are necessary.
- tsimionescu 1y agoThis is actually a relatively common example of the benefits of either polymorphism (for the a.add(b) version) or generic methods/templates. You'll actually find libraries that have a method or template function like this in C++, Java, C#, and other commonly used static languages.
- o11c 1y agoThe critical problem with structural typing is that it requires weird and arbitrary branding when dealing with unions of singletons.
- b_e_n_t_o_n 1y agoYou mean like if you have two types which are identical but you want your type system to treat them as distinct? To me that's a data modelling issue rather than something wrong with the type system, but I understand how it can sometimes be unavoidable and you need to work around it. I think it also makes more sense in immutable functional languages like clojure. Oddly enough I like it in Go too, despite being very different from clojure.
- andyferris 1y agoIf I understand you correctly - in popular structurally typed languages, sure. It seems ok in upcoming languages with polymorphic sum types (eg Roc “tags”) though?
- whilenot-dev 1y agoBranding doesn't need to be weird and arbitrary, see Pythons NewType: https://docs.python.org/3/library/typing.html#typing.NewType https://docs.python.org/3/library/typing.html#typing.NewType Reading TFA now, Pythons NewType seems to be equal to Haskells newtype. Yes, it's a hack for the type checker to work around existing language semantics and feels unergonomic at times when Parse, Don't Validate needs to fall back to plain validation, but I wouldn't call it neither weird nor arbitrary.
- o11c 1y agoPython defaults uses nominal typing so isn't prone to this problem anyway. The kind of "branding" I'm talking about is a hack only needed for structural typing systems. Consider something inspired by the C locale API, for example: class RealLocale: name: str const C_LOCALE = RealLocale("C"); # Each of these can be passed to *some*, but not all, locale functions, # which will check for them by identity before doing the logic for `RealLocale`. singleton NO_LOCALE # used for both "error" and "query" singleton THREAD_LOCALE singleton GLOBAL_LOCALE singleton ENV_LOCALE In a structural typing system, it is impossible to write a function that takes a union including more than one of `{NO_LOCALE, THREAD_LOCALE, GLOBAL_LOCALE, ENV_LOCALE}`, since they have no contents and thus cannot be distinguished. You have to hack around it, by some kind of casting and/or adding a member that's not actually present/useful at runtime. And this kind of need is quite common. So I maintain that structural typing is not a serious type system proposal. (Then again, the man proponent for structural typing is TypeScript, which even in $CURRENTYEAR still lacks even a way to specify ubiquitous needs like "integer" or "valid array index").
- lmm 1y agoI think that's backwards - ultimately everything on a computer is just bytes, so if you push that philosophy to the limit then you would write untyped functions and they can "just work" on any input (just not necessarily giving results that are sensible or useful if the input is wrong). The point of a type system is to help you avoid writing semantically wrong code, to bring errors forward, and actually the most important and valuable use case is distinguishing values that are structurally identical but semantically different (e.g. customer ID vs product ID, x coordinate vs y coordinate, immutable list vs read view of mutable list, sorted vs unsorted...).
- b_e_n_t_o_n 1y agoI think the structural type approach leans heavily into the "computation is just data and its transformations", so it makes sense for it to treat data as the most important thing. You end up thinking less about classification and more about the transformations. I'm not saying the nominal approach to types is wrong or bad, I just find my way of thinking is better suited for structural systems. I'm thinking less about the semantics around product_id vs user_id and more about what transforms are relevant - the semantics show up in the domain layer. Take a vec3 for example, in a structural system you could apply a function designed for a vec2 on it, which has practical applications.
- valenterry 1y ago> I think the structural type approach leans heavily into the "computation is just data and its transformations" But it's never "just data". My password is different in many ways than my username. Don't you ever log/print it by accident! So even if structurally the same, we MUST treat it different. Hence any approach that always only looks at things structurally is deeply flawed in the context of safe software development.
- deleted 1y ago[deleted]
- b_e_n_t_o_n 1y ago
- seanmcdirmid 1y agoStructural type systems mostly don’t support encapsulation (private members that store things like account numbers) without some sort of weird add on, while nominal type systems support encapsulation directly (because the name hides structure). The canonical example is a cowboy and picture that both have a draw method.
- b_e_n_t_o_n 1y agoBoth Go and TS are structural and support encapsulation fine, I'm not sure why that would be an issue.
- seanmcdirmid 1y agoTS doesn’t really. TS simply treats private fields as public ones when it comes to structural type checks. TS is unsound anyways, so not providing hard guarantees about field access safety is right up its alley. More to the point, if you specify a class type with private fields as a requirement, whatever you plug into that requirement has to have those private fields, they are part of the type’s public signature. To get where structural type systems fall down, think about a bad case is when dealing with native state and you have a private long field with a pointer hiding in it used in native calls. Any “type” that provides that long will fit the type, leading to seg faults. A nominal type system allows you to make assurances behind the class name. Anyways, this was a big deal in the late 90s, eg see opaque types https://en.wikipedia.org/wiki/Opaque_data_type https://en.wikipedia.org/wiki/Opaque_data_type.
- b_e_n_t_o_n 1y agoTypescript had to support JS's quirks... :/ class Foo { public bar = 1; private _value = 'hello'; static doSomething(f: Foo) { console.log(f._value); } } class MockFoo { public bar = 1; } let mock = new MockFoo(); Foo.doSomething(mock); // Fails Which is why you'd generally use interfaces, either declared or inline. In the pointer example, if the long field is private then it's not part of the public interface and you shouldn't run into that issue no?
- rendaw 1y agoIf someone encodes "Meter" and "Yard", your type system wouldn't provide any errors if a meter is used in a yard calculation or vice versa. If someone encodes "RGBColor" and "LinearRGBColor", both structs with 3 floats, your type system wouldn't provide any errors if a LinearRGB color is passed into an RGB calculation. You also wouldn't have any error if you accidentally passed a Vertex3 (again, struct of 3 floats) into your RGB calculation. Also, preferred by who?
- ashton314 1y agoThere 100% are unit systems implemented with types. (One example: https://github.com/goldfirere/units/tree/master/units https://github.com/goldfirere/units/tree/master/units) Structural types does not preclude having some names that prevent mix-ups. Haskell’s `data` keyword doesn’t let you confuse structurally-identical things. > If someone encodes "RGBColor" and "LinearRGBColor", both structs with 3 floats, your type system wouldn't provide any errors if a LinearRGB color is passed into an RGB calculation. It 100% would, unless you were silly enough to use a bare tuple to do it. Again, defining a type with `data` in Haskell wouldn’t get confused.
- mrkeen 1y ago> Structural types does not preclude having some names that prevent mix-ups. Haskell’s `data` keyword doesn’t let you confuse structurally-identical things. Haskell doesn't let you confuse structurally-indentical things because it is nominal, not structural.
- b_e_n_t_o_n 1y agoPreferred by me, I'm not trying to speak for anyone else. In fact I'd say it's a somewhat minority opinion. When talking about types like `Meter` and `Yard` in a structural system, the "type" of the data is also data. In a nominal system that data is encoded in the type system, but that's not the only place it can be. For example, if I asked you how far the nearest gas station is, you wouldn't respond with "10", but rather "10 minutes", or "10 kilometres", etc. Both the value and unit of measurement are relevant data, and thus both of those would be part of the structural type as well. { unit: yard, value: 20 } This is real, concrete data that you can see all at once. You can feed it into different functions, create aliases for it (unlike objects where you'd need to make snapshots or copies when they might change), compare it with other data to check equality, transmit it across networks, and work with it easily in other programming languages since they all understand basic types. When you stick with this kind of data, you can use general-purpose functions that work on any data rather than being locked into specific methods tied to particular types or interfaces - methods that won't exist when you move to different languages or systems. In a nominal system you might end up with a generic Measurement<T> type that contains the unit inside, which can help with code reuse but it's not at the same level as pure data.
- ashton314 1y agoWhat type system are you thinking about? Is there a specific language you have in mind? Haskell is structural—it emphasizes pattern matching on type variants and whatnot—but it wouldn’t confuse two different structs.
- mrkeen 1y agoHaskell is nominal. In this context structural typing is the ability to confuse two different structs.
- nivertech 1y ago> everything is represented by a bitstring, a function shouldn't care what the name is of the bitstring passed to it, it should just work if it has the correct bits. ;) Nominal type system can be built on top of structural type system with zero runtime overhead, but not vice versa (you'll have to add tags, which will take additional memory space). The problem with nominal type systems, is that it needs to support parametrized types, otherwise it's hard to impossible to write reusable / generic code.