3 ms·
IF T is int|None, then None|T is None|int (and int|None), it's just a logical union of all possible values. You're complaining against issues of some weird Rust
by nlitened 3y ago
IF T is int|None, then None|T is None|int (and int|None), it's just a logical union of all possible values. You're complaining against issues of some weird Rust-enum-wrapper-like unions, which is exactly a problem proper unions types resolve.
- lmm 3y ago> IF T is int|None, then None|T is None|int (and int|None), it's just a logical union of all possible values. Which is confusing and introduces subtle bugs. It makes it impossible to reason about any part of the code in isolation, because you can't understand the behaviour of None|T unless you know what T is.
- nlitened 3y agoI think it’s only confusing if you use None as a _magic value_ with a meaning dependent on context. Like if you use -1 as a magic value of an int which means not a negative one (which you can do arithmetics on) but as “infinity” or “default value” or “use alternative method of calculation instead”. I think it is known that magic int values always come back to bite you later. Same with None. None means “absent value”. If you use it like that, no confusion at all. Absent value or int is absent value or int. However, if in some places None means “absent value”, in others “error value”, in others “infinity”, in others “empty collection”—due to programmer’s laziness instead of using proper types, then _surely_ it will be impossible to reason. But not due to union types, I think.
- lmm 3y ago> I think it’s only confusing if you use None as a _magic value_ with a meaning dependent on context. Which is essentially every use of None? Like, the whole point of an option type, an X | None, is that None is not an X, and means something different. > I think it is known that magic int values always come back to bite you later. But the reason a magic int is a problem is because it's also a valid value. If you use -1 as a magic value, you will get confused because you can't tell whether it was whatever magic meaning you meant or the actual value -1. The whole point of using int | None is to avoid that problem, because None is never a valid int. Unfortunately if you have inclusive unions then that breaks down - you use T | None because None is never a valid T, but then if you try to use that with T = int | None, whoops. > Same with None. None means “absent value”. If you use it like that, no confusion at all. Absent value or int is absent value or int. What does that mean? I don't think there's any universal notion of "absent" that applies to every function in a single program, much less every program. What None means is context dependent, sure, but that's fine as long as that context is local; after all, what e.g. 3 means in your program is also context dependent (maybe it means "3 users" or "position 3 in the array" or "file not found"). If you have proper nesting options, then maybe you'll compose together three layers and in the end you have some value where None means "not cached" and Some(None) means "error computing" and Some(Some(None)) means "empty collection" - but that's absolutely fine, each layer knows how to handle its own option and knows what None means in that context. The problem only comes when you have union types, because then you can't compose your layers without them interfering with each other.
- nlitened 3y ago> Like, the whole point of an option type, an X | None, is that None is not an X, and means something different. Absolutely not! X|None does not mean X is never None. It's just a logical union (∪) from school. In this case it means "all possible values of X and also None if it was not in X". For example, a function may take something like "Indexable<T> | Iterable<T>", and it is fine if it is passed Vector<T> which is _both_ Indexable<T> and Iterable<T>, the sets are not disjoint. I think this is the root of our mutual misunderstanding. > I don't think there's any universal notion of "absent" that applies to every function in a single program, much less every program. I agree it's sometimes hard to maintain the same semantics over a codebase, but that's because software architecture work is hard. Number 5 should mean approximately the same over the entire codebase, and for sure programmers will find a way for it to mean different things in different parts of the code, but that's what makes it a sloppy code that is difficult to maintain! Similarly, if None means different things in different parts of a program, that's not a fault of mathematical logic or union types, that's just sloppy programming!
- lmm 3y ago> Absolutely not! X|None does not mean X is never None. It's just a logical union (∪) from school. In this case it means "all possible values of X and also None if it was not in X". Types are not sets, and thinking of them as sets will lead you astray. > For example, a function may take something like "Indexable<T> | Iterable<T>", and it is fine if it is passed Vector<T> which is _both_ Indexable<T> and Iterable<T>, the sets are not disjoint. Only because you don't care whether Vector<T> is processed as an Indexable<T> or an Iterable<T> - which is because you know that it implements both in a way that's consistent with each other, which is because you know there's a relationship between those two interfaces. But that kind of relationship ought to be expressed in the type system (in this case Indexable<T> should probably be a subtype of Iterable<T>), at which point you don't need to use a union at all. The key use case for a union U | T is when the two types U and T are unrelated. And in that case, if you passed a type that happened to implement both U and T, you would very much care about whether it was processed as a U or as a T. > I agree it's sometimes hard to maintain the same semantics over a codebase, but that's because software architecture work is hard. > Number 5 should mean approximately the same over the entire codebase, and for sure programmers will find a way for it to mean different things in different parts of the code, but that's what makes it a sloppy code that is difficult to maintain! > Similarly, if None means different things in different parts of a program, that's not a fault of mathematical logic or union types, that's just sloppy programming! A codebase has to work up from the generic to the specific. Ultimately programming is the art of translating a business problem into a bunch of 1s and 0s, it would be absurd to demand that every 1 or 0 has the same semantics everywhere in your program. Just as at the very low levels you have code that interprets a bitpattern as a number or a character or an enumeration, at a slightly higher level you'll have code that interprets a collection as meaning exclude/exclude/transform or a value as meaning target/default/.... Particularly in library code, you don't necessarily know what the objective semantics of the values you're working on are. And all that's fine and normal - for most code, the internals of the value you're working on are and should be a black box - e.g. a sort function doesn't and shouldn't know or care whether the values it's sorting are numbers or strings, or whether one string is alphabetically before or after another - all it knows is that it has a collection and a way to compare elements of that collection. You should be able to use the same sort function to sort a collection forward that value withor in reverse, even though those are the exact opposite of each other. It only becomes a problem if you mix up your layers - e.g. if you somehow pass a bitpattern that was meant to represent a number to a function that thinks it was meant to represent a string, or if your sort function confuses the magic value that was returned from the comparator with one of the values it was meant to be sorting. That's not (just) sloppy programming, it's poor language design, because you shouldn't even be able to make that kind of mistake.