5 ms·
What does it mean for None|T to be disjoint?
by tomjakubowski 3y ago
What does it mean for None|T to be disjoint?
- lmm 3y ago"If it's None then it's not T". It's very natural to write code that handles the case where the value is None, handles the case where the value is T, but subtly malfunctions if T can be None (e.g. you might write a cache and use None to represent the value not being present in the cache - but then your cache silently fails to cache if the thing you were caching returns None).
- squirtlebonflow 3y agoHow is it natural to write code like that? If it's None, don't put it in the cache. If T is None, don't put it in the cache. if (typeof input != None) putInCache(input)
- lmm 3y ago> If it's None, don't put it in the cache. If T is None, don't put it in the cache. > if (typeof input != None) putInCache(input) Exactly, now you've just written exactly the bug I was talking about.
- nyanpasu64 3y agoIf T is int|None, then None|T can't distinguish between None and T(None), whereas None|Some(T) can distinguish between None and Some(T(None)).
- Gunax 3y agoIt seems like the issue then is that int can be nullable. The solution is to make int an int. You cannot assign None to int. I'm not an expert on programming languages, but asaik this is how typescript operates
- codebje 3y agoThat's not quite what the parent is trying to say. Assume that int is not None. Create a type called T that is int|None - a term of type T can be an int, or it can be None. Now have a function whose argument is T|None. Substitute in the definition of T and you get (int|None)|None. If the function's argument is None, what does that mean? Was it given a T that happened to be None, or was it not given a T? Nobody knows.
- scubbo 3y ago> Was it given a T that happened to be None, or was it not given a T? Hmm. It _feels_ like a function shouldn't ever need to differentiate between "what the caller thinks it has" (that is - being passed a "T-which-is-None" should have the same significance as being passed a "None"), but I'm not confident claiming that would ever be the case.
- codebje 3y agoConsider an expensive computation that may have no answer, and so it returns (after some heavy processing) "int|None". Also consider a cache lookup function that takes some key and returns a generic T|None for T the type of cache entries, with None signifying the key was not found. Both are, on their own, pretty reasonable things to have. If you are using union types and you put the results of your expensive computation into your cache, you will not be able to tell when you've done an expensive computation for some key and got a None result, or when you haven't got a result in the cache for that key.
- kaba0 3y agoYou can have both union types and sum types (imo ideally), or in this case can easily choose to represent the result of the expensive computation in a more reasonable/descriptive way.
- codebje 3y agoPerhaps the more reasonable/descriptive way to represent the potentially no-value result of the computation is with a sum type, not a union type?
- nlitened 3y agoIF 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.