5 ms·
Using "Maybe" as a positive example of what Haskell can do isn't right. Say you have a function with input of type A and output of type B, written (A -> B). The
by cubefox 2y ago
Using "Maybe" as a positive example of what Haskell can do isn't right. Say you have a function with input of type A and output of type B, written (A -> B). The problem with Maybe ("option" types) then is that if you have a function, in production use, of type (X -> Maybe Y), you can't "weaken your assumptions for the input and strengthen your promises for the output" (which would be an improvement) and rewrite it to the type (Maybe X -> Y). Because then you would have to modify all the code which already uses the function. Since "A" and "Maybe A" are incompatible types. Which is illogical.
Several other null-safe languages solve this correctly by allowing disjunctions of types (often called unions, though countless other type related things are also called unions). They have a type operator "|" (or) and the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y). Code outside the function doesn't have to be changed: Accepting X or Null implies accepting X, and returning Y implies returning Y or Null.
- dsign 2y agodata Maybe a = Nothing | Just a deriving (Eq, Ord) >> Because then you would have to modify all the code which already uses the function, as "A" and "Maybe A" are incompatible types. Which is illogical. I'm not so sure about your statement. If the type of the function changes, revising its usage at every use point is good for your sanity. I would go further and say that sometimes Maybe X is too weak, because it doesn't contain precise semantics for its Nothing and Just x alternatives. For example, sometimes you want a `Nothing` for a value that hasn't yet been found, but could potentially be filled up the evaluation chain, e.g. `NothingYet`, and a different Nothing for a value that is conclusively not there, e.g. `TerrifyingVoid`. If you fork your `Nothing` value into these two variants after you discover the need for it, you will have to revise each call anyway, and check what's the proper course of action. And this is a feature I wish I could use from Python. More generally, in large Haskell code bases, having the type-checker help you track, at compile time, code that breaks far away from where you made your changes, is an incredible time-saver.
- cubefox 2y agoYes, there are edge cases where you would like to have multiple different "kinds of null", but those use cases seem so uncommon in practice that they are mostly irrelevant.
- magicalhippo 2y agoI don't use Haskell, so this is a dumb question. Why can't X be implicitly cast to Maybe X?
- tobz619 2y agoIt can using pure or return or if working with just Maybe specifically then Maybe is defined like so: data Maybe a = Just a | Nothing So to make an X a Maybe X, you'd put a Just before a value of type X. For example: one :: Int one = 1 mOne :: Maybe Int mOne = Just one -- alternatively, pure one since our type signature tells us what pure should resolve to. Reason we can do this is because Maybe is also an Applicative and a Monad and so implements pure and return which takes an ordinary value and wraps it up into an instance of what we want.
- cubefox 2y agoIsn't that explicit casting? Implicit casting would be automatically performed by the compiler without the need to (re)write any explicit code.
- ninkendo 2y agoSounds similar to how you need to do Some(x) when passing x to something expecting an Option in rust. Swift interestingly doesn’t require this, but only because Optionals are granted lots of extra syntax sugar in the language. It’s really wrapping it in .some(x) for you behind the scenes, but the compiler can figure this out on its own. This means that in swift, changing a function from f(T) to f(T?) (ie. f(Optional<T>)) is a source-compatible change, albeit not an ABI-compatible one.
- magicalhippo 2y ago> mOne = Just one I'd call that explicit casting. Implicit casting would be mOne = one Compiler already knows what "one" is, it could insert the "Just" itself, no? Possibly due to an operator defined on Maybe that does this transformation? That is, are there some technical reasons it doesn't? Or is it just (no pun inteded) a language choice?
- tobz619 2y agoIf you have a value "aValue :: a" and a monadic function of "mFunc :: (a -> Maybe b)" that's essentially just asking you to use `>>= :: Maybe a -> (a -> Maybe b) -> Maybe b` as well as `pure :: Applicative f => a -> f a` which will lift our regular `aValue` to a `Maybe a` in this instance. Then to get the result "b" you can use the `maybe :: b -> (a -> b) -> Maybe a -> b` function to get your "b" back and do the weakening as you desire. `Maybe` assumes a computation can fail, and the `maybe` function forces you to give a default value in the case that your computation fails (aka returns Nothing) or a transformation of the result that's of the same resultant type. Overall, you'd end up with a function call that looks like: foo :: b foo = maybe someDefaultValueOnFailure someFuncOnResult (pure aValue >>= mFunc) or if you don't want to change the result then you can use `fromMaybe :: a -> Maybe a -> a` bar :: b bar = fromMaybe someOtherDefaultValueOnFailure (pure value >>= mFunc) -- if the last computation succeeds, return that value of resultant type of your computation
- cubefox 2y agoPerhaps that theoretically solves the problem, but it sounds awfullly complicated in practice.
- HelloNurse 2y agoThis is fine and understandable in theory, but a usability disaster in practice. If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid, except perhaps for a dead code warning on the parts that deal with the now impossible case of a missing result from f. You are suggesting not only a pile of monad-related ugly complications to deal with the mismatch between b and Maybe b, which are probably the best Haskell can do, but also introducing default values that can only have the practical effect of poisoning error handling.
- the_af 2y ago> If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid Why do you need to change the type signature at all? You "improved" [1] a function to make impossible for the error case to occur, but it's used everywhere and the calling code must handle the error case (I mean, that's what static typing of this sort). So there you have it: the client code is not rendered invalid, it just has dead code for handling a case that will never happen (or more usually, this just bubbles up to the error handler, not even requiring dead code at every call site). As an aside, I don't see the problem with the "pile of monads" and it doesn't seem very complicated. ---- [1] which I assume means "I know I'll be calling this with values that make it impossible for the error to occur". If you are actually changing the code, well, it goes without saying that if the assumptions you made when choosing the type changed when re-writing the function, well... the calling sites breaking everywhere is a strength of static typing.
- kyjasdfwus 2y agoScala seems to be the only language that recognizes the merit of having both unions and ADTs. It even has GADTs!
- tobz619 2y agoHaskell also has GADTs: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gadt.html https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gadt..., firstly as an extension and now it's built into the latest version of this year's compiler
- kyjasdfwus 2y agoYep, Scala is influenced by Haskell.
- iso8859-1 2y agoYou're misinterpreting 'GHC2024'. It's just a language edition, a short hand of enabling a bunch of extensions. You have been able to enable GADTs for many years now, with just a single pragma. It has been built in to GHC for all these years.
- cubefox 2y agoYet ironically Scala is not null safe I believe.
- vips7L 2y agohttps://docs.scala-lang.org/scala3/reference/experimental/explicit-nulls.html https://docs.scala-lang.org/scala3/reference/experimental/ex...
- cubefox 2y agoOpt-in is better than nothing, but in practice I assume this sees little use because it breaks compatibility with old code. Null safety (and type safety in general) has to be present in a programming language from the start; it can't realistically be added as a feature later.
- pyrale 2y ago> if you have a function, in production use, of type (X -> Maybe Y), you can't "weaken your assumptions for the input and strengthen your promises for the output" (which would be an improvement) and rewrite it to the type (Maybe X -> Y). 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: y :: Int -> String (fromMaybe "defaultValue" (map y (Just 3)) > 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.
- cubefox 2y ago> If your value of Y is predicated on receiving an X 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.
- pyrale 2y agoThe mainstream is languages that will happily accept null as anything, and crash at runtime. Sure, union types are cool, but they aren't expressible in most languages, while the optional construct is. Haskell'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.
- solomonb 2y agoSo in your hypothetical language with union types `X | Null -> Y` is a function that can actually return `Y | Null`? Why would you want to allow that as an implicit behavior? This would make for surprising and unclear error handling requirements. One of the main points of encoding error information in the type system in the type system is that it forces you to account for it when you modify your code. By "weakening" your assumptions on your function to allow it to produce Null values you have introduced a new requirement at all your call sites. Everywhere that you call this function now needs to handle the Null value. Its a GOOD thing that Haskell forces you to handle this via the type system.
- cubefox 2y ago> So in your hypothetical language with union types `X | Null -> Y` is a function that can actually return `Y | Null`? No, but a function which returns Y | Null can be replaced with a function which returns Y without changing code on the call site. Imagine I always used to give you a nail (Y) or nothing (Null), and you can handle both receiving a nail and receiving nothing. Then I can, at any time, change my behavior to giving you nails only. Because you can already handle nails, and the fact that I now never give you nothing doesn't bother you. You just perform a (now useless) check of whether you have received a nail or nothing.
- solomonb 2y ago> a function which returns Y | Null can be replaced with a function which returns Y without changing code on the call site. Yes this falls out of injectivity. > the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y) I agree that any value received by the former function (`X`) can be received by the latter function (`X|Null`). However you cannot rewrite the former to have the signature of the latter. You would need to write: prf : (X -> Y|Null) -> X|Null -> Y You would have to be able to convert a `Null` value into a `Y`. You could definitely use `X|Null -> Y` to implement `X -> Y|Null` but that is not what you are claiming.
- cubefox 2y ago