2

I know some people consider fail to be a mistake, and I can see why. (It seems like MonadPlus was made to replace it). But as long as it is there, it seems like it would make sense for partial functions to use fail and pure instead of Just and Nothing. As this would allow you to do everything you can currently do and more, so something like safeHead could give you the traditional Just/Nothing or instead you could return [x]/[].

From what I have read about MonadPlus it seems like it would be better than using fail. But I don't know enough about it to say for sure, and it also would probably involve pulling into Prelude, which might be a good idea, but would be a larger change than just using fail.

So I guess my question is why partial functions don't use fail OR MonadPlus, both seem better than using a concrete type.

duplode
  • 33,731
  • 7
  • 79
  • 150
semicolon
  • 2,530
  • 27
  • 37
  • So something like `safeHead :: Monad m => [a] -> m a; safeHead [] = fail "safeHead: empty list"; safeHead (x:_) = return x`? That seems like it would defeat the purpose a bit since you'd still have to remove a layer of wrapping. – David Young Apr 14 '16 at 22:52
  • 4
    Not quite an answer, but: `fail` is pretty much universally considered to be a wart, and is really only there so partial pattern matches in `do` notation can be desugared correctly (`do [x] <- getArgs ; …` calls `fail` with a pattern match error if `getArgs` doesn't return a single-element list). Nothing new uses it, and it's (finally!) getting removed in GHC (albeit slowly, for backwards-compatibility reasons) in [the MFP (i.e., the `MonadFail` proposal)](https://prime.haskell.org/wiki/Libraries/Proposals/MonadFail). – Antal Spector-Zabusky Apr 14 '16 at 22:54
  • Incidentally, if that is what you're talking about, that is exactly the behavior you will get when you have something like `do { (x:_) <- aList; ...}`, since `fail` gets called automatically on pattern match failures in `do` blocks. – David Young Apr 14 '16 at 22:56
  • @DavidYoung umm... suggests for `safeHead` have ALWAYS worked that way. Going from `[a]` to `a` can never be total, so if you want a `safeHead` you have to have `[a] -> [a]` or `[a] -> Maybe a` or something like that. – semicolon Apr 14 '16 at 23:46
  • @AntalSpector-Zabusky what is the difference between `MonadFail` and `MonadPlus`? Would `MonadFail` enable what I am talking about? Or would it perhaps be better to use `MonadPlus`? – semicolon Apr 14 '16 at 23:47
  • @semicolon: `MonadFail` will be a class with a monad constraint that provides the method `fail :: String -> m a`, designed, AIUI, for error-like failure. Sometimes it'll be more like `MonadPlus`, with `fail = const mempty` – but if `fail` uses the argument, then the two must be different. `fail` is for these more exceptional conditions. If `headMay :: [a] -> Maybe a` were to be generalized, I would expect it to become `headAlt :: Alternative f => [a] -> f a`, but I don't know if that's actually better, which is why this isn't an answer. (`Alternative` is like `MonadPlus` for `Applicative`.) – Antal Spector-Zabusky Apr 14 '16 at 23:52
  • @semicolon That's not what I'm asking. I mean do you mean to give it *literally* the type signature `safeHead :: Monad m => [a] -> m a` (with the `Monad m =>` constraint)? – David Young Apr 14 '16 at 23:58
  • @DavidYoung I don't see how that is any worse than `safeHead :: [a] -> Just a`, which is a subset of that type signature. So yes... yes I do. – semicolon Apr 15 '16 at 00:15
  • @AntalSpector-Zabusky `Alternative` looks interesting, it makes me wonder what the point of `MonadPlus` is actually. Because shouldn't `empty` = `mzero` and `<|>` = `mplus`? Hell MonadPlus has a minimal complete definition of `Nothing`. Is that one of those historical warts? But yeah `headAlt` or `safeHead` using Alternative sounds amazing, and would be exactly what I would like to see. – semicolon Apr 15 '16 at 00:24
  • @AntalSpector-Zabusky Is there any subset of Alternative that doesn't require `<|>` for things that are partial but never involve multiple results. Like `head`? – semicolon Apr 15 '16 at 00:25
  • @semicolon: (1) On the one hand, `MonadPlus` is a bit of a historical accident (from before `Applicative` was a superclass of `Monad`), but even so, some things are `Alternative` that aren't `MonadPlus`; `MonadPlus` is a promise that even stronger laws hold. See [my long answer here](http://stackoverflow.com/a/13081604/237428) and [AndrewC's answer to my question here](http://stackoverflow.com/a/13124491/237428). …[1/2] – Antal Spector-Zabusky Apr 15 '16 at 00:29
  • @semicolon: …[2/2] (2) No, there's not; `empty` is mostly meaningful as it relates to `(<|>)`. (Although it could have laws relating it to `(<*>)`… but regardless, such a class doesn't exist.) Also, remember that `(<|>)` is more general than just "multiple results" – for instance, `Maybe` and `STM` both have `Alternative` instances, but `Maybe` holds 0 or 1 thing, and `STM` is about concurrency. – Antal Spector-Zabusky Apr 15 '16 at 00:31
  • @semicolon I'm not saying it's worse, I'm just asking because it's a very different type. It has an extra "level" of polymorphism. If you just have `safeHead :: [a] -> Maybe a`, using `fail "anything"` is by definition the same as just using `Nothing`. If you allow any Monad what `fail` does depends on the monad that the caller of `safeHead` decides to use. – David Young Apr 15 '16 at 00:32
  • @AntalSpector-Zabusky In that case shouldn't the MonadPlus typeclass not specify any methods and solely be used for laws? Or are you saying sometimes `mplus /= <|>` or `mzero /= empty`? And the multiple results thing makes sense. – semicolon Apr 15 '16 at 05:00
  • @DavidYoung well yeah, I mean isn't a large part of the point of polymorphism changing behavior based on the type? For example `pure "foo" <|> pure "bar"` changes dramatically from when you use `[String]` to when you use `Maybe String`. I don't really see the downside. If you only use `safeHead` as a `Maybe` you should never get any surprising behavior. – semicolon Apr 15 '16 at 05:03
  • @semicolon: That would be one approach! The methods are forced to be there for historical reasons (`Applicative` wasn't a superclass of `Monad` until recently, so `Alternative` wasn't a superclass of `MonadPlus` either), so the design decision can't be made. One could imagine reasons to want different names, though – using `mplus` or `mzero` when you want monad-related laws will infer the right constraint. While you should always have `mplus ≡ (<|>)` and `mzero ≡ empty`, you might have an `Alternative` that is not a `MonadPlus`, so `(Alternative m, Monad m)` is not the same as `MonadPlus m`. – Antal Spector-Zabusky Apr 15 '16 at 05:04
  • @AntalSpector-Zabusky That is a fair point. Although if you look at another one of my questions: http://stackoverflow.com/questions/36555865/why-does-haskell-contain-so-many-equivalent-functions, you will see that my opinion is fairly strongly against such a way of doing things. Which is not to say it is horrible, because I am by no means an authority on anything when it comes to Haskell. But it would make me happy! – semicolon Apr 15 '16 at 05:14
  • @semicolon: For the ones you have in that question, it's a lot more clear-cut that the problem is just historical cruft; the extra thing going on here is two *parallel* class hierarchies, rather than one big one. So `pure` vs. `return` is always just "did we go down enough in the hierarchy". Using `(>>=)` and `pure` will infer the right constraint; using `(>>=)` and `(<|>)` will infer the *wrong* constraint. Now: it's quite possible the right decision in a vacuum would be to eliminate `mplus` and `mzero` (though of course we can't), and I'm sympathetic to that. But that's the counterargument. – Antal Spector-Zabusky Apr 15 '16 at 05:20
  • @AntalSpector-Zabusky Doesn't it end up being pretty similar though? Because if you use `(<|>)` and `(>>=)` you get a type constraint that is a superclass(es) of what you get from using `mplus`. Just like how if you use `pure` you get a type constraint that is a superclass of what you get from using `return`. – semicolon Apr 15 '16 at 07:09
  • @semicolon The difference is that using `pure` *and* `(>>=)` will give you the right type signature, promising that the laws that relate the two hold. Using `(<|>)` and `(>>=)` gets you a too-weak constraint that doesn't promise the two are related. – Antal Spector-Zabusky Apr 15 '16 at 23:08
  • @AntalSpector-Zabusky Ah, I see, and I suppose there are useful instances where `(<|>)` and `(>>=)` aren't related? So asserting that all lawful instances of both must be related is overly restrictive? – semicolon Apr 16 '16 at 02:22

1 Answers1

8

So I guess my question is why partial functions don't use fail OR MonadPlus, both seem better than using a concrete type.

Well, I can't speak to the motivations of the folks who wrote them, but I certainly share their preference. What I'd say here is that many of us follow a school of thought where the concrete type like Maybe would be our default choice, because:

  1. It's simple and concrete;
  2. It models the domain perfectly;
  3. It can be generically mapped into any more abstract solution you can think of.

Types like a -> Maybe b model the concept of a partial function perfectly, because a partial function either returns a result (Just b) or it doesn't (Nothing), and there are no finer distinctions to be made (e.g., there aren't different kinds of "nothingness").

Point #3 can be illustrated by this function, which generically transforms Maybe a into any instance of Alternative (which is a superclass of MonadPlus):

import Control.Applicative

fromMaybe :: Alternative f => Maybe a -> f a
fromMaybe Nothing = empty
fromMaybe (Just a) = pure a

So going by this philosophy, you write the partial functions in terms of Maybe, and if you need the result to be in some other Alternative instance then you use a fromMaybe function as an adapter.

This could be however be argued the other way around, where you'd have this:

safeHead :: Alternative f => [a] -> f a
safeHead [] = empty
safeHead (a:_) = pure a

...with the argument that typing just safeHead xs is shorter than fromMaybe (safeHead xs). To each their own.

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
  • 1
    I was hoping for an answer other than "to each their own". But that genuinely can be the answer for some things, as most decisions have pros and cons. I guess I just fall on the other side of the argument (the last three lines of your answer). – semicolon Apr 15 '16 at 00:13
  • 3
    @semicolon I think point 2 is an answer other than "to each their own". If you choose `safeHead :: Alternative f => [a] -> f a` (or your `MonadPlus` constraint or whatever), you do not get the promise that `safeHead xs :: [a]` has at most one element *from the type signature*, whereas `safeHead :: [a] -> Maybe a` does give you this promise. In other words, the more general type also allows buggy implementations that the more specific type does not. You might say it's hard to write such a small function in a buggy way; but the habit of writing types which prevent bugs is a good one to be in. – Daniel Wagner Apr 15 '16 at 02:37
  • @DanielWagner that is admittedly a fair point. But I mean there plenty of examples of functions that have a codomain smaller than the return type's domain, one example is `length`. I just think it is annoying if you want to get a certain container type out of a partial function that you have to start with one type you don't want and convert it, rather than just getting the right type automatically through Haskell's powerful type system. – semicolon Apr 15 '16 at 05:13
  • 1
    @semicolon There are certainly proponents of the type `length :: [a] -> Nat`. As for your complaint, I wholeheartedly agree. However, the change you propose is not free; the tradeoff that is most often discussed is that the more polymorphic type can hide genuine bugs, e.g. by allowing type inference to succeed at a type different than the one the programmer had in mind. Personally I'm more friendly to the "let it be polymorphic" side of the argument, but it's definitely clear that there is an argument; unlike some things about the language I dislike, this is not _merely_ historical precedent. – Daniel Wagner Apr 15 '16 at 06:56
  • @DanielWagner I think we are more or less in agreement then. Don't top level function explicit type declarations help avoid a lot of these issues? – semicolon Apr 15 '16 at 07:03
  • 1
    @semicolon Explicit type declarations help somewhat, but there is still type inference inside the function; if a polymorphic producer (here, `safeHead`) and a consumer are tied together locally, it is possible (even easy -- I've done it myself, and as a tangentially related example [this question](http://stackoverflow.com/q/36460833/791604) is becoming infamous) to have inference bugs which are quite subtle. – Daniel Wagner Apr 15 '16 at 07:13
  • @DanielWagner that makes sense. Although what is an example of a polymorphic produce that might sometimes make a tuple with the wrong length but sometimes won't? I don't disagree with your point as a whole, even if I haven't run into the issue much personally besides with `read`, but the example seems a little removed from the exact problem we are considering. – semicolon Apr 15 '16 at 07:37
  • @semicolon It is not easy to invent a bug which is simultaneously plausible, targeted, and small/self-contained. Suffice it to say that I and many others have observed many times where a term type-checked but in a surprising and undesired way. Another example that springs to mind is [this one](http://stackoverflow.com/q/36578438/791604), and still others come from using the lens package without fully understanding it; it is extremely easy to write lensy terms that typecheck and do something very different than what you expected. (cont'd) – Daniel Wagner Apr 15 '16 at 08:20
  • 1
    @semicolon The more things you make polymorphic, the worse the problem becomes, as you allow them to interact with each other and give the compiler more and more leeway to choose unexpected instantiations of the types. For this reason it is nice to occasionally have terms with very concrete types, to the point that many professional Haskell programmers will write explicitly monomorphized versions of otherwise polymorphic terms with trivial one-token definitions; e.g. `mapMaybe :: (a -> b) -> Maybe a -> Maybe b; mapMaybe = fmap` or similar. – Daniel Wagner Apr 15 '16 at 08:22
  • 1
    @DanielWagner Such monomorphized functions can be very useful for guiding type inference. One could generalize the type of e.g. `filter` to `(Foldable f, Alternative q) => (a -> Bool) -> f a -> q a` (and here `Alternative` models the ouput domain correctly as opposed to in the case of `safeHead`) but then things like `filter f . filter g` will no longer type check. Even when generality is strictly *correct* it may not be useful. – user2407038 Apr 15 '16 at 12:54
  • 1
    Right, you don't need to go so far as finding cases where this could lead to bugs or undefined behavior. The only purpose of generalizing `safeHead` is to avoid having to write explicit conversions, but if you go too far down that path, you end up with the "`show . read` problem" everywhere and have to write explicit type annotations instead. It's a balancing act. – Reid Barton Apr 15 '16 at 13:43
  • Another advantage to just returning `Maybe a` is not having to change the types of your functions every 10 years to keep up with current trends in type class design. (Applicative didn't even exist in 2006.) – Reid Barton Apr 15 '16 at 13:46
  • @DanielWagner By that do you mean as opposed to crashing at compile time? Because if the possible instance is unambiguous then there should be no unexpected behavior, and if it is ambiguous then it shouldn't compile. So if making something more generic makes something act unexpectedly, it wouldn't have compiled initially. Also as for the "monomorphized" (pretty sure that monomorphism is something totally different), functions, wouldn't a clean and concise syntactic construct be a better alternative to that. (`::` around the whole function is ugly), maybe like `@Maybe . map`. – semicolon Apr 15 '16 at 15:32
  • @semicolon That way you automatically get every restricted version of every function. Instead of having to manually write each one and clutter your namespace and waste an extra few lines of code every time. – semicolon Apr 15 '16 at 15:36
  • @user2407038 I am pretty sure that is not a valid type signature for filter. `Alternative` does not have a `pure` equivalent. So I have absolutely no clue how you can make an `Alternative` out of thin air. – semicolon Apr 15 '16 at 15:42
  • @semicolon Sometimes the instance is unambiguous, but the programmer has made a mistake in their thinking and the instance they think is unambiguous is different to the one the compiler thinks is unambiguous! I hope you will not take it as controversial to claim that programmers sometimes make mistakes. – Daniel Wagner Apr 15 '16 at 17:58
  • @semicolon [`Alternative` is a subclass of `Applicative`](https://hackage.haskell.org/package/base-4.8.2.0/docs/Control-Applicative.html#t:Alternative), so it has `pure` available. – Antal Spector-Zabusky Apr 15 '16 at 18:44
  • @DanielWagner Ah I see, so it is not technically completely unique to polymorphic functions, it is just a lot less likely with concrete instances, as with concrete instances you should know exactly what goes in and out of every component function. – semicolon Apr 15 '16 at 21:00