4 ms·
I'm interested in understanding your addendum because I often find myself teaching programmers new languages by mapping concepts from languages they already kno
by matt_kantor 6y ago
I'm interested in understanding your addendum because I often find myself teaching programmers new languages by mapping concepts from languages they already know.
Here's a TypeScript analogy that might help clarify things: https://www.typescriptlang.org/play?#code/C4TwDgpgBASgrgZ2AGwJYGsIEEBOOCGIAPACoA0UAclBAB7AQB2AJglI3ALYBGEOAfFAC8UHBHzMA9o2QgoJANoBdKADIoAbyjImAc2AALAFxUoAXwBQF0JFiIUGCCThgdARiJZBIsROmyoBSwla3BoeCQ0TGdXCAAmTwoAIW9RcSkZOSDkkJtw+yinFx0AZkSoJIoAYVTfDIDsiuqQgHoWqAA6LohgAGMgA https://www.typescriptlang.org/play?#code/C4TwDgpgBASgrgZ2AG...
I'm not sure what you're getting at when mentioning unions & intersections. Are you comparing a type like `Foo<number | boolean>` with `Bar<number, boolean>`? Those are conceptually very different (even in TypeScript), but maybe I'm misunderstanding.
The big difference is structural vs nominal typing and the fact that Rust doesn't really have subtyping (besides lifetimes). TypeScript types are defined by their structure—an object type you define can be compatible with a different object type from a separate library that never heard of you, just by nature of sharing the same properties. However Rust types are delineated by their names/identities—two separate types are not directly interchangeable even if they're defined identically.
- no_wizard 6y agoYeah they're definitely different. My point being that in TypeScript though, its generally assumed (unless a constraint type is used or an explicit type / interface) is used, you can generally get away with using a union or an intersection to specify a generic type value. So for instance, `Foo<number | boolean>` or `Bar<number, boolean>` wouldn't be out of place on say, a function's return type depending on what it does. Not that you should but you most certainly can. In some cases (like dealing with fetch results) its infinitely useful when doing foundational library work on a project / app / library. In other cases, I may want to use something like an intersection type to compose a return type or value type of two interfaces that are similar enough to be merged into one, or I have a composition function that merges data structures together etc. Fundamentally, with Rust, I have found, and again, (again, disclaimer: I'm really new at this language), that saying something is generic does not imply you can saturate type values like the examples I gave above (the real thing I was getting at). Its not common place to see this (at least, not that I've read or seen yet). Typically, there is a level of explicitness even within generics, like mentioned with the array. It has to be homogenous so A union type is a no go (at least, homogenous in my mind means of one uniform type). An intersection type might be idiomatic but I don't know if Rust even has this equivalent in that way. The way I think of it myself is that the compiler is trying to do the right thing not just for the developer but for the program, so these constraints likely tie back to memory safety and efficiency, so yes, the interchangeability aspect throws me for a loop when reading (it's the same words but a vastly different context) sometimes. Even C# was less strict in this way (but still a bit more strict than TypeScript, though you could just box and unbox the very annoying to see in practice most of the time `object` type since all types derive from the C# object type). F# was better in that you could do type aliasing which made certain things better (like generic Record types. I still believe F# is a better language overall in terms of ergonomics. I wonder if I could just leverage that instead of Rust, but I think the binaries would be much big (easily 50mb plus) for what I'm trying to achieve, which is web dev tools, ala things like SWC)
- matt_kantor 6y agoTypeScript-style untagged unions don't exist in (safe) Rust. Instead, "or" is done with enums (which are tagged/discriminated unions—https://en.wikipedia.org/wiki/Tagged_union https://en.wikipedia.org/wiki/Tagged_union). One difference is that in TypeScript `number | number` reduces to just `number`, but Rust enums don't work that way (`enum Foo { A(f64), B(f64) }` has two distinct variants). Check out these equivalent-ish programs: TypeScript: https://www.typescriptlang.org/play?#code/C4TwDgpgBA6gTgQzJOAeAKgPigXigbwCgoSoA3BAGwFcIAuKdQgX0MIDNqA7AY2AEsA9lyjBBAIUGDKEBFwAUADwbwkKVACMpMuVAA+ULtQC2GiHEwBKBlumyRRUlH7so80JEGvFAOgo1oHCCoACIjU3MQywJiJ1I4CGBqOBFff1ooAEJggAZYkmYoCEoAZ2hHOJIEpJSoNKpafKhWVg5uPiERbgB3RDAMTCUVPvUsa0YYp2rk1L8GiBY2HmES4CgEXAJyeYYARh8AJgBmZsJlrlWoDU38bYCGYDgM1jFJOzl5BEtCV+17eQ033Olx4NzutAYIW6AAsEMAIGRIqcen15DxLEA https://www.typescriptlang.org/play?#code/C4TwDgpgBA6gTgQzJO... type Wrapper<T> = { value: T } function toBoolean(x: Wrapper<boolean | number>): boolean { if (typeof x.value === "number") { return x.value !== 0 } else { return x.value } } function unwrap<T>(x: Wrapper<T>): T { return x.value } const a = { value: 1.23 } const b = { value: true } toBoolean(a) toBoolean(b) const c = { value: "whatever" } unwrap(c) Rust: https://play.rust-lang.org/?version=stable&edition=2018&gist=57df29387c57028e0e6c60a292835d9f https://play.rust-lang.org/?version=stable&edition=2018&gist... struct Wrapper<T> { value: T, } enum BooleanOrNumber { Boolean(bool), Number(f64), } fn to_bool(x: Wrapper<BooleanOrNumber>) -> bool { match x.value { BooleanOrNumber::Boolean(b) => b, BooleanOrNumber::Number(n) => n != 0.0, } } fn unwrap<T>(x: Wrapper<T>) -> T { x.value } fn main() { let a = Wrapper { value: BooleanOrNumber::Number(1.23) }; let b = Wrapper { value: BooleanOrNumber::Boolean(true) }; to_bool(a); to_bool(b); let c = Wrapper { value: "whatever" }; unwrap(c); } In Rust you also have traits to play with: https://play.rust-lang.org/?version=stable&edition=2018&gist=f888e94f4ce9d7b94d0cd3578946a12d https://play.rust-lang.org/?version=stable&edition=2018&gist... struct Wrapper<T> { value: T, } trait Boolable { fn to_bool(self) -> bool; } impl Boolable for bool { fn to_bool(self) -> bool { self } } impl Boolable for f64 { fn to_bool(self) -> bool { self != 0.0 } } fn to_bool(x: Wrapper<impl Boolable>) -> bool { x.value.to_bool() } fn unwrap<T>(x: Wrapper<T>) -> T { x.value } fn main() { let a = Wrapper { value: 1.23 }; let b = Wrapper { value: true }; to_bool(a); to_bool(b); let c = Wrapper { value: "whatever" }; unwrap(c); } I find there's a decent mental shift switching between TypeScript and Rust, despite some superficial similarities. I use both languages in different codebases and it usually takes me a bit to adjust when hopping back and forth. Both type systems have useful worldviews, but they're pretty different, and they nudge you towards different ways of structuring your code.