I'm not an expert on programming languages, but asaik this is how typescript operates
I'm not an expert on programming languages, but asaik this is how typescript operates
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.
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.
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.
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.
Right, neither do I - like I said, "that's an issue for the _overall system_ of a cache which stores T.". The bug is that a given type (None) has different meanings to different components of the system, and this bug only arises _because_ "One function's return value is [being passed, directly, without any interpretation, as ] another function's parameter". If the type signatures were changed so that interpretation was required - so that "the answer is None" could be distinguished from "I don't have an answer" - then the bug in the overall system goes away.
Which is the normal way of programming. If you can't safely compose functions without adding an extra layer of interpretation between them, programming becomes much harder.
> If the type signatures were changed so that interpretation was required - so that "the answer is None" could be distinguished from "I don't have an answer" - then the bug in the overall system goes away.
Which is something that using sum types rather than union types achieves by default. Wherever you want to localise the problem, union types add a big, easy class of ways to shoot yourself in the foot that just aren't there if you use sum types.
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.
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? >>> T = int|None
>>> T|None
int | None
Yep, quite simply this is solved flattening the unions and removing duplicates :)