Closed Bodigrim closed 1 year ago
What do you see as the advantage of this over using a tool like hlint
? I personally think there are times where functions will have internal invariants that are not easily captured by the type system and such functions can be perfectly safe. Moreover, if a user is likely to misuse head
they are just as likely to misuse propagating the error forward with their own pattern match (were they to see this warning but not understand the point of it).
Why only
head
andtail
? Because these are functions, for which safe concise drop-in replacements exist. E. g., forinit
/last
there is currently no such replacement (one must push for addition ofData.List.unsnoc
first), and things like!!
andmaximum
are even worse.
Can you explain further? I dont see how head
would have a safe drop-in replacement; any such drop-in replacement is necessarily partial.
All of these have a non-partial non-drop-in replacement:
headSafe = fmap NonEmpty.head . nonEmpty
lastSafe = fmap NonEmpty.last . nonEmpty
tailSafe = fmap NonEmpty.tail . nonEmpty
initSafe = fmap NonEmpty.init . nonEmpty
Note that since the flag to disable this warning is the same flag to disable deprecations, this proposal effectively deprecates head and tail, which I'm not sure we should do in a one-off operation.
We don't do this for any other partial function [in base]. I don't think head and tail are special enough to make an exception. Now, maybe if we had a push to deprecate all partial functions in base (which I'm not opposed to), I'd be on board...
I would strongly oppose this change. It completely ignores users of GHCi, educational users, and other situations in which there's absolutely zero reason to avoid head. Even if you take it on principle that using head is somehow "bad behavior" that should be punished, it also pushes users toward the wrong solution. Uses of head/tail happen when you're certain the input is non-empty, and the default solution there, whenever possible, should be change the type to NonEmpty, not to add useless boilerplate dealing with an impossible branch in your code.
All of this might be overlooked if there were a compelling benefit. But having two fewer partial functions (and, frankly, quite possibly two of the most obviously partial and therefore least problematic) doesn't change much when there are so many more remaining.
Honestly, this would feel to me like an effort to deliberately annoy users the Haskell community disagrees with and drive them away, more than a way to help anyone.
How would this affect users of Liquid Haskell?
It completely ignores users of GHCi, educational users, and other situations in which there's absolutely zero reason to avoid head.
I'd argue that you should still avoid it in these cases, especially education, since you don't want to teach people bad habits that they have to later unlearn when they start writing production code.
Uses of head/tail happen when you're certain the input is non-empty, and the default solution there, whenever possible, should be change the type to NonEmpty, not to add useless boilerplate dealing with an impossible branch in your code.
I agree with this, but I don't see why it's a reason not to make this change. (Or was this in regard to the specific text of the warning pragma, rather than its existence? If so, then I agree it should probably mention NonEmpty too.)
But having two fewer partial functions (and, frankly, quite possibly two of the most obviously partial and therefore least problematic) doesn't change much when there are so many more remaining.
Sure, it doesn't change much, but it's still a small step in the right direction.
I agree with this proposal. It's a great way to move the language forward, because it will prod both newcomers and old hands to use safer alternatives such as pattern matching.
Anecdotally, I never use head
/tail
.
What do you see as the advantage of this over using a tool like hlint?
@Boarders hlint
is great but does not ship with GHC and many beginners don't know about it
if a user is likely to misuse head they are just as likely to misuse propagating the error forward with their own pattern match
@Boarders really? What's the evidence for this?
maybe if we had a push to deprecate all partial functions in base I'd be on board...
@brandonchinn178 agree, let's do this!
Please don't do this. There are perfectly reasonable uses for head
and tail
; the most common one I use is when writing tests, where the presence of an empty list should fail the test, and throwing an exception is a fine way to achieve that.
I would be fine with adding these warnings by default if there was an easy way to disable them, but -Wno-warnings-deprecations
is too blunt an instrument. It would be different if we had something like https://github.com/ghc-proposals/ghc-proposals/pull/454 and could attach a custom warning flag, say -Wno-incomplete-functions
. (I see this was discussed somewhat before at https://github.com/haskell/core-libraries-committee/issues/70#issuecomment-1170084390.)
I fully support this proposal ๐
I'm yet to see a use case for head
/ tail
which is better than NonEmpty
/ manual pattern matching / usage of custom error
. In my experience, head
and tail
has this surprising property that it's always possible to painlessly rewrite their usages to NonEmpty
or similar.
A warning doesn't take these functions away. It just warns about common pitfalls. So I see no harm in it.
I would strongly oppose this change. It completely ignores users of GHCi, educational users
Quite the opposite, it actually helps educational users because there's a warning about potential dangers of this function.
I personally think there are times where functions will have internal invariants that are not easily captured by the type system
Note that since the flag to disable this warning is the same flag to disable deprecations, this proposal effectively deprecates head and tail, which I'm not sure we should do in a one-off operation.
Uses of head/tail happen when you're certain the input is non-empty, and the default solution there
I would be fine with adding these warnings by default if there was an easy way to disable them, but -Wno-warnings-deprecations
Haskellers would rather suppress useful warnings than improve code maintainability ๐ We all are "Make illegal states unrepresentable" unless it's a specific partial function we have strong feelings about ๐ฎโ๐จ
My suggestion to the proposal is to slightly change the wording to reflect the fact that the functions fail on empty lists (because it might not be obvious for Haskell beginners):
-{-# WARNING head "This is a partial function, use pattern matching or Data.List.uncons instead" #-}
+{-# WARNING head "This is a partial function, it throws an error on empty lists, use pattern matching or Data.List.uncons instead" #-}
I'm yet to see a use case for
head
/tail
which is better thanNonEmpty
/ manual pattern matching / usage of customerror
. In my experience,head
andtail
has this surprising property that it's always possible to painlessly rewrite their usages toNonEmpty
or similar.
The most legitimate use I've seen is the MonadFix []
instance. There's only one of those though. Head
and Tail
type families are much more useful, IME, but I recognize that head
and tail
are helpful in GHCi sessions.
(FWIW, xs ~ (Head xs ': Tail xs)
shows up relatively often.)
I think this is really highlighting the need for something like https://github.com/ghc-proposals/ghc-proposals/pull/454 in order to more painlessly make subtractive improvements to the language (and libraries as well!).
Sometimes whether the list is empty or not is ancillary to the logic at hand. Here's an incredibly trivial example:
case product xs of
1 -> foo
n -> bar n (head xs)
In the second case, we know for a certainty that the list is not empty because product [] == 1
. Furthermore, in the first case, we do not know that the list is empty, because it could have been [1, 1, 1, 1]
. The emptiness of the list is completely ancillary to the calculation we are interested in, and we would have had to make some strange custom version of product
to include a NonEmpty
in the result.
I understand that you usually shouldn't use these functions, but making it a warning seems misguided to me for three reasons.
-Wno-warnings-deprecations
for the whole module is bad.head
or tail
.head
or tail
is not commonly missed. You either meant to do it or you didn't do it.EDIT: To be clear, I'm not saying I think they should still be in Prelude
; they definitely shouldn't. But the only warning they ought to have is a deprecation warning.
when writing tests ... the presence of an empty list should fail the test, and throwing an exception is a fine way to achieve that.
It sounds like this is a good use case for a specially-named version of head
provided by the testing framework in question (e.g. expectNonEmptyOrError
). It doesn't seem justified to keep a partial function around in every[^1] Haskell module ever written just so it can be used for a small number of specialised purposes.
[^1]: barring NoImplicitPrelude
etc.
You can't disable it on a per-call basis. When you know you're using it right, you need to be able to shut the warning up. Disabling -Wno-warnings-deprecations for the whole module is bad.
What if the partial functions being deprecated were under a new flag?
Or if that's not enough... heck, give each partial function it's own flag if it clears a path to no partial functions in base.
This will negatively affect those who use -Werror
seriously
This will negatively affect those who use
-Werror
seriously
Do you mean "this will negatively affect those who use -Werror
and head
/tail
seriously"?
Well yes, and those who use Liquid Haskell too.
Regarding -Werror
, I don't agree that the effect is negative. In fact I think it's positive. People who voluntarily use -Werror
are, in effect, saying "I want the powers that be to warn me against any style of code they deem inappropriate". That's exactly what's happening here!
Regarding the interaction of head
/tail
and Liquid Haskell, I would like to hear more.
It seems to me that the proposal is predicated on the assumption that there are insignificantly few cases where the logic of a computation dictates non-emptiness, and therefore, the proponents mention ad-hoc NonEmpty
and ad-hoc (with never occurring cases) pattern matching as a means to tackle the issue. This is a code smell for those who rely on generic refinement annotations for guiding safety of their code, very often these annotations (non-emptiness facts) will be inferred even outside of direct call sites of head
/tail
.
I'm not the proposer, so I can't speak for his assumptions, but that is not my understanding. My understanding is that the assumption is that those who have genuine cause to use head
/tail
should import it from another module, not the Prelude
.
Why isn't it the other way around? Power users already know what they're doing, and for beginners there could be ghc --beginner
that enables a custom prelude with total functions only.
Firstly, I'm a "power user" and I wish head
and tail
had never been added to Prelude
. I suspect many other power users feel the same.
Secondly, power users are more likely to know how to tweak their tools to get desired behaviour, and beginners are less likely to know. That suggests that the the behaviour that requires configuration shouldn't be the one that we want beginners to be exposed to.
Firstly, I'm a "power user" and I wish head and tail had never been added to Prelude. I suspect many other power users feel the same.
I'm a power user and I know that the future is going to be refinement/dependent where partiality of head
/tail
is a non-issue.
Secondly, power users are more likely to know how to tweak their tools to get desired behaviour, and beginners are less likely to know.
beginners will not benefit from this warning, because aside head
/tail
there are many other partial functions and many safer alternatives to choose from, and the thing that the beginners lack the most is the single entry point into the ecosystem. That includes official tutorials and guideline sections marked as Start from here
. If these sections explicitly state the beginner mode, "the behaviour that requires configuration" is a non-issue either.
Sometimes whether the list is empty or not is ancillary to the logic at hand. Here's an incredibly trivial example:
case product xs of 1 -> foo n -> bar n (head xs)
It's indeed a contrived example. I'm not surprised it's possible to create an artificial example but do people actualy write the code like that?
Here's a better way to write this particular snippet in a less "smart" way, without using head
and with free performance optimization on top:
case xs of
[] -> foo
y : _
| all (== 1) xs -> foo
| otherwise -> bar (product xs) y
For particularly pedantic people who think that the previous version could be less efficient due to traversing the list twice, here's another alternative version:
case xs of [] -> foo y : _ -> let p = product xs in if p == 1 then foo else bar p y
I personally like how PureScript solved this with the Partial
type class and the unsafePartial
escape hatch.
Maybe https://github.com/ghc-proposals/ghc-proposals/pull/454 would allow a similar solution in Haskell.
I like the idea of a Partial
type class but applying it to head
/tail
would be a breaking change. The change proposed here is not breaking (except for users of -Werror
).
I'm not a fan of this proposal but I was never bothered by the existence of basic partial functions like head
and tail
, not to invalidate the concerns around partiality but I suspect the discussion will become a time sink with little to show for it.
I'm enthusiastic about removing partial functions from 'base'. Partial functions aren't the future of this language, and while the best time to start "officially" discouraging their use was 10 years ago, now is the second-best time.
beginners will not benefit from this warning, because aside head/tail there are many other partial functions
Beginners usually meet head and tail first @avanov .
Their relationship with partial functions start with this first impression.
I believe that pushing beginners away from partial functions also solves another common question that demonstrates a flawed mental model:
"How do I get the value out of the Monad? "
Restating things, the non-obvious benefit here is pushing beginners to look for the use in these "wrappings" rather than say "getting rid of maybeness".
There are perfectly reasonable uses for
head
andtail
; the most common one I use is when writing tests, where the presence of an empty list should fail the test, and throwing an exception is a fine way to achieve that.
Wouldn't using a custom pattern match with error
be better here, so that the message could say which list was empty, and explain why it's expected to always contain at least one value, instead of just saying "empty list"?
@Bodigrim are you against #70, or just see it as stalled? I did start moving things around to make a patch earlier this year, and would like to finish it. Just have been, alas, pulled in many directions.
(There is no technical difficulty making the patch, just context switched away from it :/)
FWIW I do support this one too.
As a beginner, I have to say that seeing head
and tail
used in tutorials did make me think they are "normal" and recommended Haskell code. In my opinion, it adds confusion to have to learn and unlearn them in my journey to writing production Haskell code.
More generally, I feel the fact that there is no easily discoverable de facto (as in a safe default), agreed upon, and community endorsed best practice in Haskell is what's making Haskell hard to "stick". For a beginner, the type system (or more accurately, the confusing error messages) is the a main source of friction to getting productive with Haskell, and now you add to the fact that you've been learning Haskell the wrong way for months or even years, that really adds to the frustration and helplessness of the Haskell experience.
I constantly ask myself, what is good Haskell code? Is the example that I read from this blog post or GitHub repository something I can trust/use?
If this proposal were to go through, then modulo Liquid Haskell issues, what other "negative impact" situations do people see? In the case of -Werror, if the programmer using it endorses their use of head/tail, then can't the warning be suppressed with an additional flag (when calling the compiler) or with pragmas (on the file side)? That's a cost, sure, but it seems quite small compared to the benefits that new users of Haskell would get when properly informed of the dangers of head/tail thanks to this change.
then can't the warning be suppressed with an additional flag (when calling the compiler) or with pragmas (on the file side)?
Not without suppressing all warnings and deprecations in the entire module. Like I mentioned above, adding this warning is effectively the same thing as adding the DEPRECATED pragma, since it uses the same flag to suppress the warning.
then can't the warning be suppressed with an additional flag (when calling the compiler) or with pragmas (on the file side)?
Not without suppressing all warnings and deprecations in the entire module. Like I mentioned above, adding this warning is effectively the same thing as adding the DEPRECATED pragma, since it uses the same flag to suppress the warning.
It would suppress literally all warnings even not coming from deprecated functions? So I wouldn't get the -Werror benefits of having my code fail to compile if there's unused imports? I had expected that there'd be some specific flag tied to this, be it -fno-warn-deprecations or something different, but if there isn't, then I see the argument against much more clearly. If a flag like -fno-warn-deprecations can be used to disable the warning from head being used, then I think that that's a very small cost to pay given the benefits at stake.
Thanks all for the active discussion, I've updated the proposal to answer some questions. Let me cover some more suggestions below.
I personally think there are times where functions will have internal invariants that are not easily captured by the type system and such functions can be perfectly safe.
@Boarders But we are not in a case where internal invariants cannot be easily captured by the type system, or where their validation is anyhow expensive.
We don't do this for any other partial function [in base]. I don't think head and tail are special enough to make an exception.
@brandonchinn178 As the proposal says, it's more difficult to replace other partial functions. E. g., what to do with init
and last
? Pattern-matching does not help, NonEmpty
does not help, and there is no unsnoc
, so you probably need to reverse a list twice. Or where to find a safe version of (!!)
? Once these gaps are filled by future CLC proposals, we'll be able to add warnings to these functions as well.
Uses of head/tail happen when you're certain the input is non-empty, and the default solution there, whenever possible, should be change the type to NonEmpty, not to add useless boilerplate dealing with an impossible branch in your code.
@cdsmith I'm not inventing a new warning here, just promoting the existing one, which happened not to mention NonEmpty
. I don't mind to suggest it indeed, even while it may spark a non-local refactoring.
How would this affect users of Liquid Haskell?
@treeowl I'm yet to see Liquid Haskell outside of academic papers. Liquid Haskell is for properties, which are hard to express in the Haskell type system. Does not make much sense to resort to such complex machinery to impose or validate a basic invariant.
There are perfectly reasonable uses for head and tail; the most common one I use is when writing tests, where the presence of an empty list should fail the test, and throwing an exception is a fine way to achieve that.
@adamgundry Certainly you can disable -Wno-warnings-deprecations
in your test suite without much harm? Or define your own head
and tail
in no more than 7 lines?
What if the partial functions being deprecated were under a new flag?
@ParetoOptimalDev New GHC flags are not under CLC purview.
I'm a power user and I know that the future is going to be refinement/dependent where partiality of head/tail is a non-issue.
@avanov Ah, you posess a rare gift, you know the future. Do not waste too much time on CLC discussions, the world needs you ;)
@Bodigrim are you against #70, or just see it as stalled? I did start moving things around to make a patch earlier this year, and would like to finish it. Just have been, alas, pulled in many directions.
@Ericson2314 As far as I understand #70, it depends on a 4-years-old-but-not-yet-implemented GHC proposal, and a specific discussion is not possible before that. If you get time, please finish up #10 + #84, #53 and #64 before proceeding to #70.
@Bodigrim #70's step 1 doesn't depend on any of that. I figured I would make separate MRs with separate CLC votes for each step. But fair enough on #10 coming first :).
It would suppress literally all warnings even not coming from deprecated functions? So I wouldn't get the -Werror benefits of having my code fail to compile if there's unused imports? I had expected that there'd be some specific flag tied to this, be it -fno-warn-deprecations or something different, but if there isn't, then I see the argument against much more clearly. If a flag like -fno-warn-deprecations can be used to disable the warning from head being used, then I think that that's a very small cost to pay given the benefits at stake.
@tavrinky sorry, I wasn't being very precise. Let me be clear: there's a single flag to suppress all warnings from the following pragmas:
{-# WARNING foo "foo is bad" #-}
{-# DEPRECATED bar #-}
So if we add {-# WARNING #-}
for head
, you can't suppress just the warning for head
, you have to also suppress any other WARNING
and DEPRECATED
pragmas. Which goes back to my point of this proposal effectively adds the DEPRECATED
pragma to head
/tail
.
@brandonchinn178 As the proposal says, it's more difficult to replace other partial functions. E. g., what to do with init and last? Pattern-matching does not help, NonEmpty does not help, and there is no unsnoc, so you probably need to reverse a list twice. Or where to find a safe version of (!!)? Once these gaps are filled by future CLC proposals, we'll be able to add warnings to these functions as well.
@Bodigrim did you see the rest of my comment? You said "Why only head and tail? Because these are functions, for which safe concise drop-in replacements exist." What are the safe drop-in replacements?
I don't think any "safe concise drop-in replacements" do exist. If I'm doing head xs
, there is no non-partial function foo
for which foo xs
works.
init
and last
can also pattern match:
case nonEmpty xs of
Nothing -> ...
Just xs' -> (NonEmpty.init xs', NonEmpty.last xs')
Ah, you posess a rare gift, you know the future. Do not waste too much time on CLC discussions, the world needs you ;)
@Bodigrim there's no need for a gift of any kind to see that NonEmpty
is a crutch and that a generic alternative with industrial-grade SMT solvers is out there for real. I'm pretty sure you also are aware of proposals like this one and continuously emerging topics on how wonderful it would be if GHC narrowed the gap with Agda and Idris, because <non-academia-ergonomics-reasons>
. So why pretending that there's no way people could know how the future of programming languages most likely gonna look like, especially if your direct attention-seeking competitors (Scala, Rust) are looking into the same direction as well? ;)
there's no need for a gift of any kind to see that NonEmpty is a crutch and that a generic alternative with industrial-grade SMT solvers is out there for real.
Which gets us closer to dependent Haskell being a reality?
If new Haskellers learn with and form mental models with partial functions, they'll surely be less receptive to the value proposition of dependent types...
Simply because they won't have been exposed to anything approaching it.
Which gets us closer to dependent Haskell being a reality?
Refinement constraints could be taken off the shelf and used right now, even in Python. That's enough for replacing NonEmpty
.
If new Haskellers learn with and form mental models with partial functions, they'll surely be less receptive to the value proposition of dependent types...
There are two ways how this can be addressed, and I don't see how a hypothetical ghc --beginner
makes it less ergonomic for beginners than this proposal. As I mentioned it above, there are many more obstacles that beginners face the very first minute of their interaction with GHC defaults (partial head
/tail
in the default prelude are just one instance of it), that this proposal doesn't address. Having a special beginner-friendly GHC flag that would enable all these better alternatives (-Wno-incomplete-patterns
anyone?) under a single operational mode would be a better solution to the goal of bringing beginners proper mental models. Why is it better? Because it plans in advance how safer alternatives could be extended and delivered to beginners in all future adjustments, without disrupting existing expectations of experienced users.
Hardcore fans of head and tail, who are not satisfied with disabling warnings, are welcome to create a local file or even release a package, providing, say, Data.List.Partial
But now the partiality has metastasized: to utility modules, into partial lambdas, etc.; hlint
becomes no longer useful on this lint, code review becomes more difficult.
I think this proposal would benefit brand new users, but be a nuisance for the majority of users. IMO new default warnings should expose things that are surprising.
a new LINT
pragma, that comes with an arbitrary, optional category (in this case "partial"), like:
{-# LINT partial head "This is a partial function, it throws an error on empty lists, use pattern m..." #-}
users can turn on the lint with -Wlints-everything
or -Wlints-partial
I think this would be useful
(EDIT: if I wanted to go all in on this proposal: maybe the rewrite rules mechanism could be coopted, so that e.g. composed functions could trigger library-author-defined lints)
Please don't. This will make head
unusable with -Werror
, breaking a lot of code. I'll probably have to define my own version of head
and put it in a Utils library somewhere, which is silly.
Please don't. This will make head unusable with
-Werror
, breaking a lot of code.
Simon, can you help me understand this point of view better? As far as I can tell, those who use -Werror
really, really, really want to squash as many places as possible in their code where surprising behaviour might be hiding. But head
causes exactly that kind of surprising behaviour. So isn't a warning on head
good for users of -Werror
?
here's no need for a gift of any kind to see that NonEmpty is a crutch
I'm really happy that there are people pushing on the "SMT => dependent types" front so that we're advancing with many different approaches, but for me the best thing about dependent types is "correctness-by-construction", which is exactly the thing NonEmpty
achieves. SMT solvers also don't offer nearly as good of an interactive experience as using actual DTs and correctness-by-construction afaik, so I'm not sure how you're so convinced that's the future for everyone. Regardless, this is going extremely off-topic, and my point is that your "the future is SMTs" seems extremely speculative, and like a non-argument - it could be used to diffuse any proposal for incremental improvement to the types of functions in base
.
Regardless of which of the two we go with though, the type of head
and tail
will have to change, which is a breaking change. So I really don't see how what you're saying is the future is any better than adding this warning - it breaks more code than the warning, because type errors don't compile, as opposed to warnings which can be ignored. "But my SMT solver checks it automatically, and the Haskell type doesn't need to change because my SMT solver type is separate from it" - sure, fine. But
head
would effectively change, meaning that we'll still need an incremental migration path forward (which in reality would be a prerequisite of the "always enable the SMT solver" step). If we don't always enable the SMT solver, then that means that what you're suggesting doesn't achieve the same thing as what is being suggested here.head
actually have a check associated with themhead
right now are behind a check that SMT solvers can see throughhead
is safe, the SMT solver to be able to prove that head is safe), because of turing incompletenessThere are two ways how this can be addressed, and I don't see how a hypothetical ghc --beginner makes it less ergonomic for beginners than this proposal.
Anything that is opt-in is already extremely beginner unfriendly - the beginner downloads the compiler or apt install
s it or whatever and totally ignores anything that's written anywhere. Unless you suggest for ghc to prompt the user on first installation "are you a beginner?" or something (which is still more beginner unfriendly than the thing that comes out of the box being the right thing for a beginner).
Anything that is opt-in is already extremely beginner unfriendly - the beginner downloads the compiler or apt installs it or whatever and totally ignores anything that's written anywhere.
I'm actually suggesting replacing a great many of other currently required beginner-unfriendly opt-ins with a single opt-in to toggle them all.
@Bodigrim You didn't address my point that this can be done right now using hlint
. If an industry code base cares enough about safety to enable all warnings then they should be willing to invest in setting up a hlint
config (which can do away with whatever functions they don't like).
I think a wider point is that this doesn't address the core issue that is compelling - partial functions are a total pain to debug when internal invariants are not upheld. I think the solution is probably not to add warnings to partial functions (I am not totally opposed but it sounds more like the Politician's fallacy - 'We must do something. This is something. Therefore, we must do this' - than a real solution). I prefer moving such functions to their own Partial
namespace, but only so long as GHC comes with a re-factoring plugin that can make such a change automatic (people rightly do not appreciate these paper cut re-factors in base). I also think we should take seriously the comments on Andreas Abel regarding the different type theoretic guises that [a]
is currently used as (streams, colists and lists) since these have different answers as to what is partial. Back to the wider point - the problem is with debugging Haskell code. Either much more should be written about how to effectively use the ghci
debugger, so that one can easily find a problematic call to a partial function, or we should focus on other tools (maybe ghc-debug
) that can make debugging far easier for everyday Haskell programmers (C++ programmers' tooling enable debugging far, far worse stuff than calling a partial function so it doesn't seem unreasonable to ask for better).
I don't take seriously any of the arguments about beginners (so far) or "unlearning use of the head
function" (a bizarre notion - developers have to be in the habit of learning and unlearning best practices and new ideas all the time, it is just part of the job).
You didn't address my point that this can be done right now using
hlint
.
This cannot be addressed by hlint
universally. HLint has access only to the syntax tree, no type information or module location. So it's impossible to warn "only on head
from base
" and not on some other head
(e.g. head
from relude
).
So it's impossible to warn "only on head from base"
... with HLint. As the stan
author, you know .hie
files have this information :smile:
@ocharles Yes, but I'm not maintaining stan
anymore, stan
doesn't support GHC 9+ and is generally not maintained. So unless someone wants to sponsor the development of stan
, I wouldn't gatekeep this proposal by a theoretical possibility of supporting this feature by other tooling without actually having this said tooling working.
Haddocks for
Data.List
(technically,GHC.List
) warn againsthead
andtail
on the ground of their partiality.I propose to promote these warnings to the pragma level as per MR !9290:
I do not propose any further steps such as deprecation or removal of these functions. This is deliberately as conservative as possible. See https://github.com/haskell/core-libraries-committee/issues/70 for a wider discussion of a wider proposal.
Why only
head
andtail
? Because these are functions, for which the widest range of replacements exist, almost always allowing for a safe, concise and local fix (see examples below). E. g., forinit
/last
there is currently no such replacement (one must push for addition ofData.List.unsnoc
first), and things like!!
andmaximum
are even worse.Why
{-# WARNING #-}
and not{-# DEPRECATED #-}
? Because deprecation implies a future removal, and ambitions of this proposal are much smaller. It's already enshrined inbase
that these functions deserve a warning, we just promote its visibility, which should be less controversial.The impact of the change is that users of
head
andtail
will receive a GHC warning message. This is not an error and does not prevent from compilation, thus is not a breaking change. Users are recommended to follow the suggestion, or disable-Wno-warnings-deprecations
(which is a sensible thing to do, for example, in a test suite), but they are also free to do nothing at all. Old packages will continue to work.To avoid any confusion,
-Wno-warnings-deprecations
suppresses{-# WARNING #-}
and{-# DEPRECATED #-}
, but not any other GHC warnings. Those, who enabled-Werror
, can pass-Wwarn=warnings-deprecations
to downgrade this particular group back from errors to warnings. GHCi users can put:set -Wno-warnings-deprecations
into their.ghci
config.There is a concern that
-Wno-warnings-deprecations
disables all{-# WARNING #-}
and{-# DEPRECATED #-}
, whatever the source. However, current Haskell ecosystem rarely makes much use of them, so I believe it is still a palatable compromise between seeing no warnings and making no changes.Hardcore fans of
head
andtail
, who are not satisfied with disabling warnings, are welcome to create a local file or even release a package, providing, say,Data.List.Partial
, containing original definitions ofhead
andtail
without{-# WARNING #-}
. I'm however opposed to introducing suchData.List.Partial
intobase
itself: we won't be able to root it out ever.GHC proposals https://github.com/ghc-proposals/ghc-proposals/pull/454 and https://github.com/ghc-proposals/ghc-proposals/pull/541 propose extensions to GHC warnings mechanism. Unfortunately, neither of them is approved or has a committed implementor, and this status does not seem to change soon, so it would be wrong to speculate on their precise nature. If and when they become a part of GHC, one can indeed ask for a review of
{-# WARNING #-}
pragmas.How would you rewrite
instance MonadFix []
withouthead
andtail
?I'd rewrite it this way:
How would you rewrite this snippet?
Besides options in https://github.com/haskell/core-libraries-committee/issues/87#issuecomment-1243587955, one can do this:
or (if you insist on exactly two clauses):
How would you rewrite this snippet?
I'd use a proper library for infinite lists aka streams:
Stream
,streams
orinfinite-list
. E. g.,Stream
provides totalhead :: Stream a -> a
andfilter :: Stream a -> Stream a
, so the snippet can be rewritten in a total way asinfinite-list
can make it even neater offering(0...)
syntax to replace[0..]
.