-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deprecate partial functions from the Prelude, giving them new quarantined homes #70
Comments
Strongly in favour of attempting to build community consensus around this. |
Very much in favor of only doing breaking changes to base with a migration tool. We should have the technology! |
Concerning partial functions like
These three are lumped together in a single type
I think a systematic fix of the problem should introduce a |
While I like the idea, I'm not sure it makes sense to create a |
Another thing that should be taken into account in this proposal - many learning resources introduce |
I strongly agree with what @andreasabel said - we should have, long-term, separate types for at least a finite list and streams. I think that that effort is however much larger and that this proposal might even be a step in that direction and/or part of the process (once we've cleaved the functions that are valid on |
I don't believe that adding other list-like types to Prelude should be a precondition for removing the partial versions from My reasoning is that if the Prelude did not already have these partial functions, I'm fairly confident the committee would not accept adding such partial functions if they were proposed today |
@andreasabel Yes I have wanted
That last one, while more of a meta concern, is most important to me. I think in the past we've done a bad job at understanding extent to which are conflicting priorities, and thus haven't been able to muster the energy to do things which would actually defuse the conflict (better migration tools!). I am really hoping we can turn a new leaf. |
Indeed so! |
A subtlety in @andreasabel's point is that what counts as partial depends on your viewpoint. If you primarily think of I'd also like to add a small spin to the requirements for the migration tool: that it can be called as a source plugin. That is, I want to be able to write Happily, this requirement should not be hard to accommodate, because any migration tool essentially has to do all of this work already. I'm just specifying a particular interface. Note also that GHC already has support for code transformations that are intended to preserve comments, indentation structure, etc., so phrasing the migration tool as a plugin might even simplify the task. |
Well to be really pendantic, it's fail in finite steps. data X = Foo X | Bar X
foos (Foo x) = () : foos x
bad = let x = Bar x; in foos x here I bring this up basically to say that Haskell never tries to save you from non-termination, but should try to say you from what we were calling "incompleteness" in new incomplete (missing patterns in case, missing fields in constructor, etc.) The good thing about this is that it is an objective principle, not mattering on ones interpretation of what is |
My preferred stretch goal would instead be shim libraries: if the user includes a Still this reminded me it's good to think in terms of what is blocking each step. I split step 1 into 1 and 2 to better leverage this:
This means, sure, plugin and shim base why not! What I am really concerned with is getting to steps 1 and 2. If 3 doesn't happen for years and years so be it. At the same time, I think getting to step 2 will make this process "feel real", and the psychological boost of finally dealing with this longstanding bugbear will inspire us to write whatever migration tools we deem are necessary. |
I agree with what you just wrote @Ericson2314 -- in particular, about having step 3 be far in the future. So maybe the current proposal should just have steps 1 and 2, keeping step 3 aspirational. That is, we commit to
but that's all we commit to. In general terms, the proposal can also say that we would like to actually remove the deprecated bits in the future, but that depends on a migration tool, or plugin, or reinstallable I have to admit that removing a concrete plan for (3) makes me considerably more enthusiastic about this proposal. I'm scared of (3), precisely because we don't exactly know what (3) looks like yet. |
@goldfirere Sounds great to me! Increasing enthusian at the mere cost of making (3) a bit more nebulous is a phenomenal trade as far as I am concerned :). |
@goldfirere I revised and retitled this; do let me know if it addresses those concerns or more is needed. |
I don't know if this is in scope for this proposal or not, but I wanted to ask if there was a plan for the new total replacements. I can see two main options, but maybe there are more:
My personal preference is the latter option, for what it's worth |
I don't think they are in scope or out of scope a priori, but just like with (FWIW, I think the primary replacement for these functions is arguable just good ole' pattern matching, and the situation we most want to avoid is people using partial functions because they never pattern matching. For more advanced users I agree |
The "...TODO fill out more" is doing an awful lot of work in this text. How many actual names are you proposing to remove? There's a big difference between 3 and 30, after all, and that can have an impact on where they are moved to. Now, with my stodgy conservative hat on: do we have a sense of the practical impact of these functions for actual developers? Have any of us heard of serious problems caused by the unjustified use of Here's an alternative proposal sketch that I think achieves your goal of "to guide the user, to make certain things very easy in order to encourage Haskell to be written in a certain way", but with less potential disruption. What if warnings are emitted for a set of known historical warts, but only when the language chosen is GHC20XX instead of Haskell2010? And that's it? |
Yes, in the days before they were given
A key part of the above story was that the To put this in a positive light, I want to be able to trust other programmers I've never met as much as possible. To me, the very field of programming languages is first and foremost about facilitating large scale human collaboration, and enabling more trust is a key part of that.
With all do respect, I think you are vastly overestimating the costs here. Since 3 is non-normative and deferred indefinitely, are we only proposing a software deprecation, which is a warning. No one needs to fix anything. if there were build failures, sure, I bet these developers of old libraries would be badgered just as you say. And that would be annoying. But I just don't think people take the time to write those emails for plain warnings --- the type of frustration that leads to such an email just isn't caused by anything less than a build-failure. Anyone that does care about warnings in other code I (perhaps idealistically :) ) believe is also sufficiently not in the rush to send a PR / patch rather than complaint, and that is a lot less burdensome for the upstream developer. Now, someone that got bit by a |
I find your argument for the value of giving warnings to more people convincing - thanks! Esp. without the actual deletion. |
Thanks! I will in turn add the downsides you mention to (non-normative) step 3 as thing migration tooling indeed cannot fix. edit done. |
I am strongly in favor of this effort and am really excited to see a lot of really great discussion to refine the proposal towards something we can take action on! |
Yes, this is a real problem. I'm tired of having to mentor juniors around these issues, writing style guides / best practice guides with dedicated sections to partial functions, and having to be defensive when reviewing code. While they are fine for hobby projects and one-off scripts, production code should not use partial functions because they make the code crash. |
(my emphasis) I think the proposal needs to clarify what constitutes a "migration". If we remove, say, import Data.Maybe (fromMaybe)
fromJust = fromMaybe (error "Maybe.fromJust: Nothing") which, of course, would achieve nothing at all.
I think this is mixing things together. Are you under the impression that removing partial functions from Usually, the way things go are:
This sort of cost-benefit analysis is non-trivial, and which functions are present in |
Step 3 is non-normative so I wouldn't spend too much time thinking about it (and maybe I should just remove it) but the migration guide I was thinking of was merely importing the needed function from That's OK to me however: the migration isn't supposed to make existing code better, it's just supposed to merely keep it working and I suppose also not make it worse (as inlining functions might).
I don't think anyone is arguing that we can skip teaching. The argument specifically was about new users grabbing
That's a tough question, but I don't think that's the question were trying to answer with this proposal! For me personally and all the projects I have worked the precept is, partially may be the answer but Under that precept, head bussinessItems is never acceptable. case bussinessItems of
x : _ -> x
[] -> error "the function formerly known as Data.List.head" is also never acceptable. case bussinessItems of
x : _ -> x
[] -> error "no business items. Because ...business reason... this should never happen." might be acceptable.
Now, I wouldn't want to impose this rule on the Haskell community at large, but having to write |
Thank you for clarifying, @Ericson2314. I had understood Step 3 to mean "removing head/fromJust etc." entirely from base. After re-reading, though, I can see that this was a misunderstanding on my part. I agree completely regarding using
To be clear: I also agree that discouraging the use of partial functions is valuable. However, this value must be weighed against the cost of being a breaking change. |
Sounds good and glad to hear it, @runeksvendsen. Without step 3 there is no breaking change, just warnings, so I hope we're successfully punting on that one lingering issue. |
For example LYAH intro level talks about
These functions have been part of Haskell since ... aww 1990. And part of LISP since 1960's. I haven't noticed the sky falling yet.
If that's the objective, isn't divide by zero or These functions are perfectly safe if you've first checked the list is not Really, doesn't everybody have better things to do? Do we want Haskell to turn into a bondage and discipline language? |
But why would we like to preserve partial functions undeprecated in |
I don't have a too strong opinion about moving this stuff to another module, but I do like partial functions in some circumstances and I see no problem with using them correctly. foo :: String -> String
foo str
| null str = str
| last str == ' ' = "lol"
| otherwise = "shrug" Yes, I can jump through hoops and redefine that function with If base Prelude at some point removes them, I'll simply write my own Prelude that redefines them. I also don't see how this fixes the larger problem of "I want to know that my program doesn't crash". Linters can already help with the obvious partial functions, projects have ways to enforce these things to some degree (e.g. haskell/haskell-language-server#2974). Increasing visibility about partial functions is fine: documentation, |
I don't think last :: [a] -> Maybe a
last [x] = Just x
last (_ : xs) = last xs
last [] = Nothing
foo :: String -> String
foo str = case last str of
Just ' ' -> "lol"
_ -> "shrug" This version of |
@chessai yeah, I'm not going to dig up all the examples of total use of partial functions, because there are infinite. |
I think the conservative solution to add |
It's nice to have escape hatches. Sometimes a quick and dirty hack is just more convenient, maybe for short lived code or learning projects. We should discourage patterns that have a tendency to bite you in the ass, without assuming that we always and everywhere know better than the programmer. The optimal level of paternalism is some, rather than lots. I've used Elm, a language that disallows partial functions almost entirely, and it can be a very frustrating experience. |
I wouldn't even call it escape hatch. Haskell is not a total language. Partial functions are well within the scope of the report. Making them more distinct is a good idea. But removing them doesn't make much sense, unless you want to remove partial pattern matching as well and whatnot. What would really be interesting is if GHC could tell me whether function |
One real drawback to the |
@goldfirere that's not the case here. If there is a demand for |
I started an implementation of step 1. Part of the reason of having the step is to find what in In a future split-base world, I would indeed move the module to a separate library and just have |
The problem is that many people might make Another possible way forward is to use ghc-proposals/ghc-proposals#454. This would allow the possibility that all partial functions are grouped under one warning flag, so they (and only they) could be permitted warning-free. This stops the problem I mentioned in my post, where I was worried about a user accidentally suppressing a warning that they want to see. Perhaps it's too troublesome to depend on an unimplemented feature like ghc-proposals/ghc-proposals#454, but it might also be a sweet spot in the design. |
I think I still like the separate module because it makes it shows up in code being read without diagnostics etc. Fancy warnings are great but to someone just perusing the code I think seeing |
I agree with @Ericson2314 and @goldfirere here, though I think dedicating a warning for partiality is perhaps a step too far in pathologizing their use. Making them a deliberate inclusion into one's codebase with an import in my mind is sufficient for general use. Slightly off-topic, but if GHC could detect partiality (either user-denoted or detected by the compiler), then I'd be in favor of a warning that tells one when there's a partial function in play. That to me seems more generally useful. Conversely, a totality checker and associated warnings would be similarly helpful. |
Just to add another feather to the pile in favour: HLS gets a steady stream of bug reports from people whose entire language server goes down with no diagnostics whatsoever because it hit a partial function somewhere in HLS or its dependencies. This is a) really embarrassing, and b) really hard to track down. If it was Java and we at least got a stack trace I could live with it, but as it is I can't see it as anything other than irresponsible to allow partial functions without informative errors into your application or any of your transitive dependencies. So anything that nudges the ecosystem away from them is good by me. |
My understanding is that this proposal tries to urge all Haskell users to a unified, slightly-more-total, style. Is there a possibility for this proposal to add an equivalent to Rust's 'expect' method or would this be out of scope? For context: let x = Some("value");
assert_eq!(x.expect("error message for None case"), "value"); This gives a unique 'this is impossible' assertion syntax without being overly verbose. -- This always terminates. But the important property isn't lists vs streams, it's '[1..] contains even numbers'
case safeHead $ filter even $ map (*3) [1..] of
Nothing -> error "List is infinite"
Just out -> out More to the point, without some syntactically cheap unwrapping operation I'm worried that people will be drawn to do-notation for error propagation which breaks HasCallStack safeAndGoodCode :: Ord a => [a] -> M.Map a b -> Maybe b
safeAndGoodCode ks dict = do
k <- safeHead ks
v <- dict M.!? k
pure v This has some of the fun properties of Also, since non-exporting is the theoretical end-goal, I'd be interested in how much the expected (and acceptable) ecosystem breakage would be. Since I still semi-regularly encounter AMP regressions my gut-feeling is >50% hackage packages being affected and >5% never being fixed could be realistic, but I have no idea what the actual numbers would be. |
I'm not sure the core issue here is actually partial functions. Yes, there were a few annoying instances in HLS, e.g. this one, which I actually found pretty easy to track down though. I'm not saying this is good ergonomics. A stack trace should be in place here, I totall agree... it gets much worse if those bugs are in libraries. The larger issue here is that any library function can just bottom out or throw exceptions, even in the absence of partial functions. This is particularly true when dealing with filesystem (which HLS does a lot... and those errors can also be pretty annoying like the infamous commitBuffer: invalid argument on windows). Here I believe it's up to the application to ensure that it doesn't crash (compare with most webservers, which don't crash on handler exceptions or even partial functions either) and employ strategies to recover from unexpected failure. |
I was actually thinking of this issue: haskell/haskell-language-server#3002 The only reason we made any progress on this is because Pepe happened to be there and remember "oh, I maybe used that function in this dependency I wrote once". If we hadn't had that we'd have been pretty stumped. Throwing exceptions from filesystem code that runs in IO is much more okay: we can and do catch those exceptions. Random failures from pure code are much more problematic. I do agree that the problem is not partial functions per se, but rather casual use of partial functions that provide no error context. If we lived in a culture where every use of partial functions was accompanied with a careful comment explaining why it was safe, and an informative error message... that might be okay. |
I think this is a much more useful goal! |
I think this proposal would move us towards that world. |
Relude's flipped Just "value" ?: error "error message for Nothing case" Maybe that could find its way into base. |
I agree. It's one thing to have a goal and quite another to have a concrete plan of steps that might help towards that goal. |
@Bodigrim I do not think this is blocked on a GHC proposal, I think this is blocked on me making a step 1 merge request for the CLC to vote on. |
@Ericson2314 the proposal says very clearly:
But besides that I'd like to see how #87 and #114 turn out in the wild before trying a different approach, as it might be unneeded after all. |
Given that there does not seem to be any progress or activity on https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0134-deprecating-exports-proposal.rst and other constraints, let's mark this as dormant for now. Rest assured that this is not a sign of dismissal, it's just that we strive to keep the list of open issues actionable. |
Problem
Partial functions like
head
are too easy to use. The purpose of thePrelude
is to guide the user, to make certain things very easy in order to encourage Haskell to be written in a certain way. The problem is that the partial functions are bad, as is widely agreed. It is unclear if there are any circumstances in which as they ought to be used, and yetPrelude
makes them extremely easy to use at present, especially by new users that don't know what pattern matching is.Many would-be solutions have been floated in the past, but I dislike them as tepid half-measures that skirt around the problem rather than confront it head on. The problem is that these partial functions are too readily available; the solution must be to make them less readily available. The solution is to make using those in the
Prelude
automatically raise red flags, and ideally to someday remove them from thePrelude
altogether.Solution
We want to move these functions with a very long deprecation cycle.
Prerequisite
https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0134-deprecating-exports-proposal.rst must be implemented first.
Steps
1. Relocate functions, with reexports
This we can do immediately!
Create
Partial
modules and move functions there:Data.List.head
->Data.List.Partial.head
Data.List.tail
->Data.List.Partial.tail
Data.Maybe.fromJust
->Data.Maybe.Partial.fromJust
Create non-deprecated reexports (since we lack the ability to do anything better) in their current locations (
Data.List
,Data.Maybe
, etc.).The purpose of this is allowing one to switch sooner once the deprecation mechanism is ready without running afoul of the 3 release cycle.
2. Deprecate Reexports
This we can do once the GHC proposal is implemented.
Prelude
reexports those and thus inherits the deprecation.3. (Non-normative) After many releases, and good tooling, consider removing reexports
We're not sure what is needed to do this, so just including as an aspirational goal / food for thought.
At the very least, this could only happen after:
3 release policy is met, so we must have both 3 past GHCs and
base
s which have these functions available from the thePartial
modules. (probably would be far more than 3 releases.)Tooling exists to automatically perform migration guide
Perhaps also some sort of "
base
shim" so code explicitly depending on the old standard library (as opposed to say Haskell just dropped in GHCi without metadata) never stops working.It is important to remember that not everyone that signs up to publish open source wants to be stuck maintaining it for life. Some people want to make a thing, release it, and wash their hands of it ---- think e.g. artists aren't expected to accept "patches" against a finished painting! Any hard breakage, no matter the migration tooling around it is going to to result in people pestering those once and done authors who will rightly ask "why is stuff in the language report for crying out loud changing out from under on me?"
For that use-case, only something like the base-shim that requires no changes in existing code --- essentially making this not a breakage change but just changing the defaults for new projects --- is acceptable. Indeed a social contract that a) we can and will retroactively add more and more warnings to old standards b) we will never change the meaning of old standards, is probably a good way to navigate these competing concerns in general.
Migration Guide
Merely import and
...Migration
module that provides an in-use function gotten from a legacy deprecated location.Note these properties:
import qualified Data.List.Partial as Partial
, butimport Data.List.Partial
is fine.Prelude
import is not necessary. One may choose to do e.g.import Prelude hiding (head)
, but this line can be omitted entirely.import Data.List.Partial (head)
, butimport Data.List.Partial
is fine.These properties follow from the nice benefits deprecated reexports provide, benefits we do not have with today's means of doing a deprecation cycle.
The text was updated successfully, but these errors were encountered: