We didn't assume it is. Say you have a function of type (String -> String|Null). Further assume that you realize you don't necessarily need a String as input, and that you in fact are able to always output a string, no matter what. This means you can rewrite (improve!) the function such that it now has the type (String|Null -> String). Relaxing the type requirements for your inputs, or strengthening the guarantees for the type of your output, or both, is always an improvement. And there is no logical reason why you would need to change any external code for that. But many type systems are not able to automatically recognize and take advantage of this logical fact.
> > Several other null-safe languages [...] returning Y implies returning Y or Null.
> I have trouble seeing how the language is null-safe in that situation.
If you always assign a value of type Y to a variable of type Y|Null, the compiler will enforce a check for Null if you access the value of the variable, which is unnecessary (as the type of the variable could be changed to Y), but it can't result in a null pointer exception.
If `Y` is implicitly `Y|Null`, then it is no longer possible to declare "this function does not return null" in the type system. Now understanding what a function can return requires checking the code or comments. This is the opposite of null safe.
It isn't. It's just that if you say "this function returns Y or null", and it returns Y, your statement was true. If you give me a hammer, this implies you gave me a hammer or a wrench.
I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?
> I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?
Any language which supports "union types" of this sort, e.g. Ceylon or, nowadays, Typescript.
# old
function f(x: number): number {
return 2 * x;
}
# new
function f(x: number|null): number {
x = x || 3;
return 2 * x;
}
# usage
# old
f(2)
# new
f(2) # still works!
But in Haskell this requires changing the call sites: -- old
f :: Int -> Int
f = (*2)
-- new
f :: Maybe Int -> Int
f = maybe 0 (*2)
-- usage
-- old
f 2
-- new
f (Just 2) -- different!
But I actually feel this is an antipattern in Haskell (and maybe TypeScript too), and a separate wrapper function avoids refactoring while making things even more user friendly. -- old
f :: Int -> Int
f = (*2)
-- new
fMaybe :: Maybe Int -> Int
fMaybe = maybe 3 f
-- usage
-- old
f 2
-- new
f 2 -- still works!
fMaybe Nothing -- works too!
Here's some wrappers for general functions (not that they're needed, they're essentially just raw prelude functions): maybeIn :: b -> (a -> b) -> (Maybe a -> b)
maybeIn = maybe
maybeOut :: (a -> b) -> (a -> Maybe b)
maybeOut = fmap Just
maybeBoth :: b -> (a -> b) -> (Maybe a -> Maybe b)
maybeBoth d = maybeOut . maybeIn d
Added bonus, this approach avoids slowing down existing code with the null checks we just added. maybeOut :: (a -> b) -> (a -> Maybe b)
maybeOut = (.) Just
Also the parenthesis around the last two output types are added for emphasis, but can be safely removed. const maybe = <T,>(f: (_: T) => T, d:T) => (x: T|null) => f(x || d);
console.log(maybe((x) => 2 * x, 3)(null)); // returns: 6Haskell's type system definitely is a positive example of what can be done to avoid completely the null problem. Is it the utmost that can be done? No. But it's been a working proof of solution for 20 years, while proper typecheckers for union types are a recent thing.
Falls short compared to what? Arguably, if you're talking to someone using Java or Python, Maybe is plenty enough; and getting started on type families is certainly not going to work well.
If your value of Y is predicated on receiving an X, I have trouble seeing how you would write such a function. If you have a default value, then Haskell would solve it just like any language with optionals:
> Several other null-safe languages [...] returning Y implies returning Y or Null.I have trouble seeing how the language is null-safe in that situation.