5 ms·
If 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)).
by nyanpasu64 3y ago
If 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?
- deleted 3y ago[deleted]
- scubbo 3y ago> 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 This is certainly an issue, but that's an issue for the _overall system_ of a cache which stores T. My claim wasn't that "Union types that Union with None can never cause issues", but rather that, for a function whose argument is `(int|None)|None`, there shouldn't ever be any different behaviour _of that function_ between "passed a T (int|None), which was None", and "passed a None (not a T)". The function itself should still behave the same way. You're right, of course, that _returning_ an (int|None), where "None" might mean "the answer is definitively known to be None" or might mean "the answer is unknown, and that is represented as None" can lead to unnecessary recomputation - but that's an issue of return types, not of parameter types.
- lmm 3y agoOne function's return value is another function's parameter. If the overall system has a bug, that's a problem, and I don't see any value in trying to quibble about whether it's a bug in the caller or the callee.
- 3y ago
- js8 3y agoIt is useful to differentiate this, though. Consider a datatype of map with values of type T. The function to return value for a key returns None when the key is not in the map, but what if the key is in the map, but the actual value is None? IIRC Lisp has a workaround but it requires special code to handle maps that can have None as values. As the other posters have said, it (automatically flattening the Maybe monad) is non-composable and should be considered a bad language design, like it was a bad idea to automatically flatten lists in Perl.
- bombolo 3y agoThat's why in python that raises a KeyError, or if you use explicitly .get() you can specify anything as your default value.
- codebje 3y agoIt's probably subjective, but I don't see the need to wrap your code in a try/except block as an improvement over an optional sum type, and I really don't see the use of a magic value to indicate no value as an improvement over anything.
- bombolo 3y agoIf it's not a try it's an if... you still need to handle the case where the key wasn't present and figure something out.
- lmm 3y agoWhich is why you end up with the painful sentinel = object() value = dictionary.get(key, sentinel) if(value == sentinel) ... Which, sure, it works, but it's working around a problem that didn't need to exist in the first place. Why not just have Option work the way you expect, and be able to contain None the same way as it can contain any other value?
- bombolo 3y agoWell no, just handle KeyError :D
- bombolo 3y agoLet's try this in python… >>> T = int|None >>> T|None int | None Yep, quite simply this is solved flattening the unions and removing duplicates :)
- fhars 3y agoYes, that is the problem, imagine 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
- bombolo 3y agoAs I already wrote in a different comment, KeyError and None aren't the same.
- dkersten 3y agoThat’s not “solved”, that’s highlighting what the problem is.
- squirtlebonflow 3y agoI don't think you should write code where it makes a difference if the None came from a T that happened to be None or from a None.
- lmm 3y agoWhy would you ever use T | None otherwise? As far as I can see the only reason to ever use T | None is because you want to write code that does one thing if it's a None and another thing if it's a T. Having it do the other thing if that T happens to be None makes no more sense than having it do the other thing if that T happens to be 5; at best it might work by accident, more likely it will be a subtle bug.
- 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.