Closed Bodigrim closed 11 months ago
@chshersh The situation you are describing involves using prelude
's head
function and also using an alternative prelude which re-defines head
to be safe whilst also keeping a hlint
file to ban uses of head
. If a company is playing that level of multi-dimensional chess I would suggest they stop.
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 (maybeghc-debug
)
We have only so much labour available. Who is going to write this putative documentation or tooling?
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. Buthead
causes exactly that kind of surprising behaviour. So isn't a warning onhead
good for users of-Werror
?
The rule of thumb for -Wall
is that these are warnings for things that are unambiguously bad and easy to avoid, whereas I don't think head
reaches that bar. Sure, in some projects I want to avoid all uses of partial functions and I would love to have a -Wpartial
, but in other projects I'm completely happy for internal invariant violations to call error
. I just want to be able to make the choice, and putting an unconditional warning on head
seems inappropriate.
A bit more philosophically, sometimes I'll use head
when correctness is clear but expressing it (using say NonEmpty) would be awkward, other times I'll use it when I probably should be using NonEmpty
but I'll treat the non-emptiness of the list as an unchecked invariant. There are often many unchecked invariants, and of course we should aspire to have fewer, but it's not always that straightforward. I don't think being super strict about this one particular partial function is really a consistent way to approach that problem.
Thanks @simonmar that's very helpful. I agree broadly with your comment. However, it doesn't provide any tools for objective reasoning about this proposal. How do we determine whether head
reaches the bar for "things that are unambiguously bad and easy to avoid"? You think it doesn't; others think it does. As a CLC member who will vote on this proposal, is there any better rationale that I can apply than merely a judgement call?
head
and -Werror
). Is that enough to reject the proposal? For me personally, probably not.let !x = head y in
and case y of (x:_) ->
. Is there a strong reason to warn on the latter but not the former? I don't see one yet.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
-Werror
doesn't mean that it warns against all warnings (it doesn't imply -Weverything
). Users of -Werror
want the ability to opt-out from undesired warnings and opt-in to the desired ones.
One point against the proposal is simply that it is a change and will break some users (those who use head and -Werror). Is that enough to reject the proposal? For me personally, probably not.
To me it seems that the main pushback against this proposal is the fact that custom {-# WARNING #-}
and {-# DEPRECATE #-}
pragmas go under the same flag -Wdeprecations
. I'm really confused why this happened in the first place because it doesn't make any sense to me. Though, I don't think we should discard improvements based on poor design decisions taken in the past.
If it were a different warning, not enabled by default (and -Wdeprecations
are enabled by default according to the documentation), I'm pretty sure nobody cared because nobody wouldn't even notice.
I never needed to use head
and tail
in my code. Not in tests, not in cases where I wanted to express list non-emptiness, not when it was easier. Sure, I have very limited Haskell experience: only 7 years of using Haskell in teaching, mentoring, blogging, production and OSS in dozens of completely different projects. It maybe just anecdotal but at least to me it suggests that it's extremely rare to have situations where head
and tail
are really much easier to the point that their usage becomes unavoidable (and I'm still yet to see an example to prove me wrong).
In any case, should we optimise for extremely rare cases or should be support the most common case?
I would greatly benefit if others would be more discouraged to use these functions. I see these warnings as a small step toward encouraging people to rely less on partial functions. I like when the compiler has my back. I have more trust in GHC telling me "this list is definitely not empty" than in another person telling me "I swear, this list is definitely not empty".
I like the sentiment of this proposal, but I don't think it is worth it, even with a custom flag like -Wpartial
The discussion in this thread has mostly covered the downsides of accepting this proposal, but I really don't think the advantages are meaningful enough.
Experienced Haskell programmers don't use head
and tail
unless they know what they are doing anyway, so this proposal wouldn't be very beneficial for them. Even if an advanced programmer wants to be extra sure not to call these accidentally, they can always use an hlint rule or a custom prelude that hides partial functons.
Instead, the main benefit of this proposal would be discouraging beginners from using partial functions, but I don't think it accomplishes that very well.
Beginners rarely enable -Wall
, let alone custom warning flags, so putting this behind either of those will probably not have a significant effect.
Therefore, the only way for this proposal to have a positive impact would be to enable these warnings by default, which is nearly equivalent to deprecating head
and tail
for everyone who uses -Werror
.
I don't think the breakage from something this drastic is warranted for the rather small positive effect it might have on beginners.
the breakage from something this drastic
I think this is less drastic than simply removing head
and tail
from Data.List
and letting people who use this poor programming pattern find their own way to shoot themselves in the foot. Don't you?
I think this is less drastic than simply removing head and tail from Data.List and letting people who use this poor programming pattern find their own way to shoot themselves in the foot.
+1
I think it's worth being explicit that this proposal is the single smallest, gentlest step we could take toward further discouragement of head
/tail
. If we're not going to do this then we're going to do nothing.
There is one "gentler" thing we could have, which is still to have -Wpartial
or -Wheadtail
(or maybe some custom warning) that's on by default. So that other deprecation warnings are not affected when someone wants to turn these off. It feels like a critical requirement for this proposal to me.
I suppose the precise warning is up for debate but I still don't understand why one would want to disable the warning for head
but not for \case (x:_) -> x
. Can someone enlighten me?
I fully support this proposal. IMHO it is making Haskell a better place.
The question I find relevant here is: would you have head
and tail
in base
if you were to redesign it from scratch? My personal opinion is that they do not belong there and we, as a community, should make what we can to get there.
And trying to warn the user about the possible issues you get by using these partial function is one way we have to start getting there
The question I find relevant here is: would you have head and tail in base if you were to redesign it from scratch?
Thanks for asking this question. I think it's an important one. I would be interested to hear from people who believe that if head
and tail
were (counterfactually) not in Prelude
then we should add them. Are there such people? If not then I find it hard to justify not making the smallest possible nudge to discourage their use.
I suppose the precise warning is up for debate but I still don't understand why one would want to disable the warning for
head
but not for\case (x:_) -> x
. Can someone enlighten me?
The non-exhaustive pattern warning catches the case where you added an extra constructor and forgot to add a pattern match.
However, it doesn't provide any tools for objective reasoning about this proposal. How do we determine whether head reaches the bar for "things that are unambiguously bad and easy to avoid"? You think it doesn't; others think it does.
I mean, we have to use reasonable judgement here. I think it's pretty clear from the controversy on this issue that there is not universal agreement that head
is unambiguously bad. And it's also fairly clear that it's not "easy to avoid" - unless you count manually inlining its definition - since it might require a significant refactor to plumb through a NonEmpty or whatever.
What about:
-- v1
head :: [a] -> a
tail :: [a] -> [a]
{-# WARNING head, tail "The types of `head` and `tail` will change to return Maybe in the next version of GHC. To dispel this warning, use `safeHead` or `safeTail` and adapt the types. If you want the old behavior, use `unsafeHead` or `unsafeTail`." #-}
unsafeHead :: [a] -> a
unsafeTail :: [a] -> [a]
safeHead :: [a] -> Maybe a
safeTail :: [a] -> Maybe a
-- v2
head :: [a] -> Maybe a
tail :: [a] -> Maybe [a]
unsafeHead :: [a] -> a
unsafeTail :: [a] -> [a]
safeHead :: [a] -> Maybe a
safeTail :: [a] -> Maybe a
-- v3
head :: [a] -> Maybe a
tail :: [a] -> Maybe [a]
unsafeHead :: [a] -> a
unsafeTail :: [a] -> [a]
safeHead :: [a] -> Maybe a
safeTail :: [a] -> Maybe a
{-# DEPRECATED safeHead, safeTail "The `safe` variants are deprecated, as the normal variants now have the safe behavior." #-}
A clear transition from "unsafe functions" to "safe functions" with easy error messages to guide migration.
The non-exhaustive pattern warning catches the case where you added an extra constructor and forgot to add a pattern match.
[]
isn't going to gain an extra constructor!
@parsonsmatt I suspect that plan would be highly controversial even if the cycle were extended longer than one GHC release, although I personally like the idea a lot.
we have to use reasonable judgement here.
Agreed, but I wish I had a clearer idea what that is.
I think it's pretty clear from the controversy on this issue that there is not universal agreement that head is unambiguously bad.
That's certainly true, although I still haven't fully grasped the arguments of those who say it's not unambiguously bad.
And it's also fairly clear that it's not "easy to avoid" - unless you count manually inlining its definition - since it might require a significant refactor to plumb through a NonEmpty or whatever.
I'm not sure I agree here. Inlining the definition of head
is about as easy as a code transformation could possibly be.
Here is a simplified version of where I have thought to use head
in what felt like an almost reasonable use case:
getDuplicate :: NonEmpty Text -> Maybe Text
getDuplicate ts | length ts = length (nub ts) = Nothing
getDuplicate ts | otherwise = head . filter (\xs -> length xs > 1) . group . sort $ ts
I am not saying this is the best code ever (far from it!) and I think one would be better off handling all cases and returning a Maybe
which is what I would actually do. The point of the code is only to indicate that NonEmpty
is not a panacea for tracking whether a list is non-empty as it might involve rather complicated evidence (in this case, knowing that a filter
will produce a non-empty list due to preconditions I have checked - in fact there would be little need for something like liquid haskell were it obvious how to use NonEmpty
to handle all cases).
I think I would prefer the suggestion of @parsonsmatt (especially if might come with a plugin for re-factoring as was discussed in the previous ticket - this would go a long way towards easing the concerns of people who see a change like this as busywork for existing programs). It could also perhaps come with a {-# UNSAFE #-} pragma and
-Wunsafe` flag or something like that.
The problem is with reasoning by example in this discussion is that simple examples are generally trivial to convert to a safe form, in this case
getDuplicate =
listToMaybe
. mapMaybe (\case (x:_:_) -> Just x; _ -> Nothing)
. group
. sort
(FWIW I much prefer the safe version), and complex examples are both hard to state and also highly unlikely to actually benefit from unsafe head
/tail
invariants holding at a distance.
getDuplicate = go Set.empty
where
go seen (t :| ts)
| t `Set.member` seen = Just t
| otherwise = case nonEmpty ts of
Nothing -> Nothing
Just ts' -> go (Set.insert t seen) ts'
Writing it safely can also give performance benefits (replace Set.member
with List.elem
if you want to stick to base
). These are skills learned by avoiding partial functions and using stronger alternatives that the core libraries already provide.
I already indicated that the example was easy to fix, that I preferred the safe version and the purpose was to explain that one can't always "strengthen the codomain" (since it might rely on arbitrary value-level evidence). I wish people wouldn't jump on the irrelevant here - it makes it seriously offputting to bother participating in the discussion.
The point you made was that "it's easy to want to use head
even though it's bad". It's easy to want to eat junk food all day even though it's bad. Why not have the compiler nag us that we're eating junk food? I'd rather be aware that I'm making a bad decision so that I can improve my decision-making.
A clear transition from "unsafe functions" to "safe functions" with easy error messages to guide migration.
I'm very much against calling such functions "unsafe", that should be reserved for actually unsafe functions that may violate type safety or memory safety, such as unsafeCoerce
or unsafePerformIO
.
Partial functions are perfectly safe in that regard, they just throw exceptions.
The point you made was that "it's easy to want to use head even though it's bad". It's easy to want to eat junk food all day even though it's bad. Why not have the compiler nag us that we're eating junk food? I'd rather be aware that I'm making a bad decision so that I can improve my decision-making.
Please don't be condescending about it, that was not the point I made at all. I am not advocating junk food - I'm not even against the proposal per se. I am just trying to broaden and participate in a discussion. If you want a heated internet argument then please do so elsewhere.
functions that may violate type safety
Isn't that literally what head :: [a] -> a
is doing?
the purpose was to explain that one can't always "strengthen the codomain"
I'd love to see an example where one can't. Presumably any evidence that a list is non empty must is equivalent to knowing that the list is a cons cell, and therefore it's equivalent to knowing that it's equivalent to NonEmpty
. Making good use of that fact may involve non-local code transformations, but I personally can't think of a realistic case where that can't be done. The existence of Liquid Haskell suggests otherwise though, so perhaps an example of such could be presented.
functions that may violate type safety
Isn't that literally what
head :: [a] -> a
is doing?
No, head someList
is always a valid a
, even if that a
is a bottom.
I'm struggling to find the full motivation for this proposal. I understand that head
is partial, and understand that the documentation warns about this partiality. But what has motivated the need to turn that into more? Is there strong evidence that people would benefit from this warning? I'm also looking at this from a problem->solution POV, and have not yet fully seen the problem that's being solved.
There's a lot going on in this thread, and I see some things that could pass as motivation, but on closer inspection look more like conjectures. I am not saying this to be antagonising. but there's a lot we could do to make Haskell better, but almost everything comes at a cost now. It's easier to justify that cost if we can point to real problems, as we then get a cost/benefit ratio.
The non-exhaustive pattern warning catches the case where you added an extra constructor and forgot to add a pattern match.
[]
isn't going to gain an extra constructor!
Sure, but you asked why someone would not want to disable the warning for \case (x:_) -> x
, and the answer is that this is -Wincomplete-patterns
which we want because it catches the case when you added a constructor.
Inlining the definition of head
Oh! So you consider inlining the definition of head
to be a reasonable way to silence the warning? But that achieves precisely nothing, doesn't it?
I'm very much against calling such functions "unsafe", that should be reserved for actually unsafe functions that may violate type safety or memory safety, such as
unsafeCoerce
orunsafePerformIO
.Partial functions are perfectly safe in that regard, they just throw exceptions.
If you use unsafeCoerce
right, then everything is fine. If you use it wrong, then your program dies with a core dump.
If you use unsafePerformIO
right, then everything is fine. If you use it wrong, then your program may duplicate effects, or perform effects in a weird order, or blow up at runtime with an exception.
If you use head
right, then everything is fine. If you use it wrong, then your program may blow up with an exception of type ErrorCall
with a stringly typed error - could have been almost anything - and you get next-to-no support in finding where it happened. (Yes, I know about HasCallStack
, it is of extremely limited use).
If you use safeHead
right, then everything is fine. It is literally impossible to use it wrong.
If you use map
right, then everything is fine, and again, it is literally impossible to use it wrong.
I really prefer writing code that is impossible to use wrong. Or, if it is possible, then I have a clear visual identifier. I also like knowing when to use unsafe code right - like, unsafePerformIO
- it says "unsafe", so I study it, and I verify it works, and I guard against other possible problems.
If something is a footgun, it needs to be properly labeled.
Inlining the definition of head
Oh! So you consider inlining the definition of
head
to be a reasonable way to silence the warning? But that achieves precisely nothing, doesn't it?
It achieves less than nothing: it costs work to do it. But the point of this proposal[^1] is not to help those who want to continue using head
or head
-like code, so we shouldn't be surprised that this proposal costs some work for those who want to keep using partial functions. Rather, the point of the proposal is to nudge people away from using (these particular) partial functions. It's up to each user to determine how they respond to the nudge.
[^1]: As I understand it. I am not the proposer, just trying to interpret the intent of those in favour.
Inlining the definition either results in an incomplete pattern match, which also gets a warning, or an error message which can be customized with more details, such as why the author thinks that case will not happen.
If something is a footgun, it needs to be properly labeled.
I have no problems with calling head
a footgun, just with calling it unsafe. Other unsafe functions like unsafePerformIO
(you can also implement unsafeCoerce
using unsafePerformIO
, so there are bigger dangers than duplicate effects and throwing an exception) and unsafeCoerce
are a different level of unsafe, you may even get code that is silently wrong (or worse, only wrong sometimes). An exception at least consistently throws (if the thunk is evaluated) and can't silently corrupt your memory and you get a stack trace (which is at least better than what you get for a segfault or getting no error at all).
Inlining the definition either results in an incomplete pattern match, which also gets a warning
Ah yes, whoops, perhaps I misunderstood @simonmar. You'd have to inline with a case that also handles the []
branch. Even more costly!
Alright, here's a concrete suggestion.
Make a GHC proposal to extend the syntax of WARNING
pragmas to include a flag, so that you could write
{-# WARNING_partial head "This is a partial function... " #-}
Here partial
is an arbitrary string specifying a class of warnings. This class of warnings would be enabled by a flag like -Wwarning-partial
or something like that.
Whether these would be enabled by -Wall
or not is up for debate, but they wouldn't be on by default.
-Wincomplete-patterns
(not on by default), which seems appropriateDoes that seem plausible?
I'd be in favor of this feature, as there are many classes of domain specific warnings that could be expressed this way (among them, instability, see https://github.com/ghc-proposals/ghc-proposals/pull/528#issuecomment-1244557447). Essentially everytime using a module or function requires particular care beyond the obvious.
@lnnf107 says
the only way for this proposal to have a positive impact would be to enable these warnings by default, which is nearly equivalent to deprecating head and tail for everyone who uses
-Werror
But {-# WARNING -#}
s are warned about by default aren't they? If they're not could someone clarify as a matter of importance?
here's a concrete suggestion. Make a GHC proposal to extend the syntax of WARNING pragmas to include a flag ... Does that seem plausible?
It certainly seems plausible. What would be your time estimate for it to progress through the stages of someone to propose this GHC feature to ghc-proposals, refine it based on steering committee feedback, wait for a vote, and then wait for someone to implement it?
I am very surprised by the controversy that this proposal has generated. I always assumed that the vast majority of Haskellers had a strong desire to avoid partial functions wherever possible, even to the extent of writing more code, and that they considered the inclusion of partial functions in base
, let alone Prelude
, to be a historic mistake, arising from a time when functional programming adhered more closely to its untyped, Lisp roots, and we hadn't yet become fully aware of the benefits of strongly typed pure functional programming. It seems that is not the case. The consequences of this revelation will take some time to sink in, for me.
I am a CLC member and will be voting on the proposal. My personal point of view is that partial functions do not belong in base
, in any codebase that I write, or preferably, any codebase that I use. I believe that Prelude
is a particular dangerous place for partial functions to live.
On the one hand, whilst I will never vote for a new partial function to be added to base
, changing the status quo is not so easy, not least for backwards compatibility reasons. This comment is a good faith effort to summarise the arguments in favour, the arguments against, and my personal responses to the arguments against, partly in order to clarify my own thinking, partly to help others in the discussion.
The controversy sparks a great fear in me: the cost at which Haskell has achieved (very modest) success is tolerance of partial functions in base
and their wide use in the ecosystem forever.
The quotations here are an amalgam of various comments in this thread. Some comments have been merged so that it looks two separate comments have been written by one person. Sorry about that but it seemed like the easiest thing to do with limited time and energy available.
"I" always refers to the original commenter. To disambiguate, @tomjaguarpaw refers to me, the current commenter.
This proposal is the single smallest, gentlest step that CLC alone could take to nudge users away from partial functions in Haskell. If we're not going to do this then it's likely the community as a whole will do nothing.
It will prod both newcomers and old hands to use safer alternatives such as pattern matching. I'm yet to see a use case for head
/ tail
which is better than NonEmpty
/ manual pattern matching / usage of custom error.
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.
A warning doesn't take these functions away. It just warns about common pitfalls.
This will make head
unusable with -Werror
, breaking a lot of code.
@tomjaguarpaw says: This seems like the most compelling argument because it's the only case I am aware of in which existing code will break.
It's pretty clear from the controversy on this issue that there is not universal agreement that head
is unambiguously bad. I understand that head is partial, and understand that the documentation warns about this partiality. But what has motivated the need to turn that into more? Is there strong evidence that people would benefit from this warning?
@tomjaguarpaw: Indeed! This is very surprising to me. I would have thought that most Haskellers want to avoid partial functions, almost axiomatically.
There are perfectly reasonable uses for head
and tail
. Some of my code does know that this list isn't empty. Whether the list is empty or not is ancillary to the logic at hand.
@tomjaguarpaw says: I have not seen a compelling example yet
There is no way to disable the warnings on a per-call-site basis. I don't want to add -Wno-warnings-deprecations
to disable because those warnings are useful.
@tomjaguarpaw says: Then don't disable them. There's nothing wrong with seeing warnings. They are informational not compile-aborting (unless you are using -Werror
, see above).
Use hlint
or other static analysis tool, or adapt GHC to have finer-grained warnings, instead.
@tomjaguarpaw says: That is out of scope for a CLC decision. If maintainers of some static analysis tool or GHC want to save the day by jumping in with an alternative, they are welcome to.
It completely ignores users of GHCi, educational users, and other situations in which there's absolutely zero reason to avoid head.
I think this proposal would benefit brand new users, but be a nuisance for the majority of users.
@tomjaguarpaw says: these two comments seem at odds regarding the impact on educational users/beginners. Is using head
good for beginners or not? The comments in this discussion seem ambivalent whether head
/tail
are good for power users but should be avoided by beginners, or vice versa.
GHCi users won't be strongly impacted unless they use -Werror
. As noted above, I haven't seen a compelling example of where there's a reason to use head
(that's not the same thing as an example where there's no reason to avoid head
).
How would this affect users of Liquid Haskell? What is the magnitude of the impact given the size of the Liquid Haskell userbase?
@tomjaguarpaw: This seems unknown. No Liquid Haskell user has come forward to volunteer an opinion.
It's not a big enough step against partial functions.
@tomjaguarpaw: Agreed, in fact quite the opposite, it's the smallest possible step against partial functions, but I'm not sure this is really an argument against this proposal.
If a user is likely to misuse head they are just as likely to misuse propagating the error forward with their own pattern match.
@tomjaguarpaw: I don't really follow this argument.
The only warning they ought to have is a deprecation warning
@tomjaguarpaw: I don't really follow this argument.
My personal point of view is that partial functions do not belong in base, in any codebase that I write, or preferably, any codebase that I use.
Just so I understand the position of people that want to eradicate partial functions - you're against array/vector indexing (you can only do fold-like things with vectors), div
and friends, error
, undefined
, recursion, and in general anything that comes with a proof obligation not expressed in the type system, is that right? Will we eventually get a warning for recursion?
(You'll have to take my word that I'm not trying to be snarky, apologies for how it sounds. I really am genuinely curious.)
[Sorry, previous comment posted before I had finished editing.]
It's a fair question. You're right if you guessed that "partiality" isn't strictly what I am opposed to. The reality is slightly more nuanced (but only slightly). The problem is that it's less easy to summarise succinctly.
Just so I understand the position of people
I don't speak for anyone other than myself here. I think plenty of others agree with me on this position, but I may be mistaken.
you're against ...
These are the following functions that I could find in base
and that I am opposed to being included in base
(that's not the same thing as saying that I will act to remove them):
(!!)
head
/tail
/init
/last
minimum
/maximum
fromJust
scanl1
/scanr1
/fold1
/scanl
read
/readIO
/readLn
fromEnum
/succ
/pred
On the other hand I'm not, per se, against:
array/vector indexing: do we even have them in base
? Perhaps in some GHC extensions. I don't really use arrays or vectors, I tend to use Seq
, and there I do indeed use !?
which runs in Maybe
rather than !!
which throws an exception.
I would be interested in further discussions about arrays/vectors and partiality. The major use case of arrays/vectors that I am familiar with is in machine learning frameworks, and there they tend to use bulk operations (folds, scans, convolutions) etc. that are total. I would need to learn more about other use cases.
div
/mod
//
and friends are tricky. I don't have a good idea about how to avoid partiality there. I don't write much numeric code so I don't have a good perspective.
The whole point of error
/undefined
is to introduce partiality, so I'm not against them.
Anything in IO
, at least until we banish (most) unchecked exceptions from Haskell (i.e. probably never)
in general anything that comes with a proof obligation not expressed in the type system
No, not as far as I'm concerned. I hope my opposition to "partiality" not tracked in the type system doesn't gives the impression I want to (or believe I can) track every property in the type system. Partiality is just a particularly cheap and easy one to track and it seems foolish not to take advantage of it. Indeed, it's the major reason I started using Haskell in the first place!
Will we eventually get a warning for recursion?
That's the prerogative of GHC not CLC, but I am supportive of efforts to be explicit about recursion. Some such proposals have already been made, for example RecursiveLet
. I've introduced plenty of bugs due to unwanted recursion and was never particularly glad I didn't have to write let rec
instead of let
.
I am very surprised by the controversy that this proposal has generated. I always assumed that the vast majority of Haskellers had a strong desire to avoid partial functions wherever possible, even to the extent of writing more code, and that they considered the inclusion of partial functions in base, let alone Prelude, to be a historic mistake, arising from a time when functional programming adhered more closely to its untyped, Lisp roots, and we hadn't yet become fully aware of the benefits of strongly typed pure functional programming. It seems that is not the case. The consequences of this revelation will take some time to sink in, for me.
Thank you for the summary of most of the major points as you see them. However, I don't see this proposal discussion in the same light as this quote and find it slightly odd as a viewpoint. I don't think weighing up the costs and benefits and alternative ideas to see the best overall path forward as controversial in any capacity, nor as endorsing something as narrow as a desire to view partial functions as fine and dandy.
In fact, I think this is exactly what a proposal process is supposed to do - to receive a wide variety of feedback and consider the points for and against. I am a bit concerned by the people that view such discussion in such a seemingly combative light since I think library and language evolution requires cool heads to carefully consider why anyone might have qualms or what other suggestions are available and then consider the best path forward in light of what has been said. This proposal doesn't read "I endorse the addition of partial functions in base", nor does it endorse "the use of partial functions are to be encouraged". I understand people can find endless workshopping of ideas frustrating and offputting but I believe it would be less so if we took the view that the feedback is supposed to strengthen the conviction whether to accept or reject a proposal when considered against the full range of views.
The paragraph you quoted was not intended to express judgement, but literally, as written, surprise. I certainly don't think the discussion has been controversial (and certainly not "combative") but I think the proposal has proved controversial. (Don't you?) That's very surprising to me! I was making a personal note about my misperception of the Haskell community. I didn't intend it to indicate anything negative about any of the participants nor the process itself.
Unlike @Boarders, I do indeed agree that one major theme of this discussion is that there is, in fact, no reason to consider partial functions universally bad. I find this particularly concerning because something I really value about the Haskell community is that it's always been very inclusive of a lot of different backgrounds and points of view. Here, though, it's almost as if there's an effort to deliberately make part of the community worse off because another part of the community disapproves of a choice they are making. That's, well, very unfortunate and concerning to me.
No one here has given any reason why head shouldn't be used when trying things out in GHCi. No one has given any reason why it shouldn't be used for education (except for "you should be educating people not to use head", which is of course just begging the question). No one has given much of a reason why it shouldn't be used when writing test code, which presumably most even "industry" and "professional" programmers spend a good bit of their time doing. The idea that, as a community, Haskell ought to agree that partial functions are always bad, regardless of the situation, as an axiom and then act on it without justifying such a bold claim is indeed precisely what I find problematic here. I enjoy being part of a community who think things through, and make the right decisions for the situation, not try to establish "best practice" by social engineering techniques that make people feel wrong without there actually being anything wrong with anything they are doing.
A second point, perhaps less controversial, that I think has been a theme here is the meaning of a warning. Like it or not, GHC emitting a warning says "I want GHC to say this not only to the person who wrote this code, but also to every single person who compiles this code, from now until the end of time." That's really quite a heavy-handed thing to do. It essentially requires that if other people are going to build your code, you do something about the warning. That can mean disabling the warning (if it's possible, and happens at the right granularity so as not to make things even worse), or it can mean changing the code. But it's pointless to claim that people who think this is okay can just ignore the warning. That isn't a reasonable choice.
In other words, -Werror
isn't for the really hardcore people who care more about following the compiler's advice. It should, in fact, be enabled in every CI system everywhere, because not enabling it means that you want other people to be warned when they are building your code, and you don't really ever want that.
I will note that the addition of HasCallStack
constraints to head
and co. has made them significantly less frustrating, as long as one remembers to add an appropriate constraint in the code that calls them as well. I don't mind code throwing exceptions where appropriate -- I just don't like it when I can't easily trace the source of those exceptions :-)
No one has given any reason why it shouldn't be used for education (except for "you should be educating people not to use head",
It shouldn't be used for education because it invites misuse (greatly among beginners) and makes things seem simpler than they are in reality.
If you teach someone something they'll use it. As beginners they'll use it even where they shouldn't.
If all goes well, that bad habit will stop. However things frequently don't all go well and all bad habits aren't unlearned or even identified.
It's very hard to let the first way you learn to do something go.
Do we want people to reach for head first as beginners or maybeHead?
In an industrial and open-source context, I'd much rather the ecosystem be made up of maybeHead.
Is there some educational context where having head is a lot better than maybeHead?
The idea that, as a community, Haskell ought to agree that partial functions are always bad, regardless of the situation, as an axiom and then act on it without justifying such a bold claim is indeed precisely what I find problematic here. I enjoy being part of a community who think things through, and make the right decisions for the situation, not try to establish "best practice" by social engineering techniques that make people feel wrong without there actually being anything wrong with anything they are doing.
I must sincerely apologise. The messages from @Boarders and @cdsmith made me realise I have communicated very badly in my message yesterday. I absolutely did not intend to imply that "as a community, Haskell ought to agree that partial functions are always bad", simply that I thought it (broadly) did. I was surprised. That's all. I joined the community, in part, because I thought it did, so to discover now, ten years later, that when push comes to shove, it doesn't, is rather a shock to me. Nonetheless, it would be greatly remiss of me tell others in the Haskell community that they are doing things wrong. Naturally, different people value different aspects of a language and ecosystem.
That said, I have asked in this thread for a better rationale that I can apply to make a decision on this proposal than merely a judgement call. Nothing has been forthcoming. I have asked for examples of code that are substantially better when written with head
. No (to me) compelling example has been forthcoming. That means as a CLC member I am being asked to vote in the abstract on a divisive issue, purely based on my own judgement, where a large proportion of the community is strongly in favour and a large proportion of the community is strongly against. This is a lose-lose situation for me personally, so I am reconsidering my participation in the CLC.
I find this particularly concerning because something I really value about the Haskell community is that it's always been very inclusive of a lot of different backgrounds and points of view. Here, though, it's almost as if there's an effort to deliberately make part of the community worse off because another part of the community disapproves of a choice they are making. That's, well, very unfortunate and concerning to me.
I don't think this is reasoning correctly. base
and Prelude
lacking things doesn't exclude anyone from writing their own libraries and preludes with whatever they want.
Standard libraries are supposed to contain the intersection of things we each like, things we can all agree on, not the union of things we each agree on. This is simply consequence of the fact that downstream libraries can add more exposed things, but they cannot remove them.
head
and tail
are reviled by a large portion of the community, and so they don't belong there. The only question ought to be how to craft a deprecation cycle.
Just so I understand the position of people that want to eradicate partial functions - you're against array/vector indexing (you can only do fold-like things with vectors), div and friends, error, undefined, recursion, and in general anything that comes with a proof obligation not expressed in the type system, is that right? Will we eventually get a warning for recursion?
I just grepped through a substantial work codebase to see if and how we use the typical partial culprits. I expected to find some "safe" uses of head (or per our in-house style, some uses of headMay where the maybe was paired with a fromMaybe (error ...)
) but found none. I did find some "safe" uses of minimum and maximum.
I think I can suggest why head and tail are less useful than other members of the "partial prelude gang". Our uses of headMay
tend to be in cases where the failure case is to be expected, and we want to explicitly give a default behavior. So an unadorned head
would tend to be the wrong thing. Our uses of maximum
are also in such circumstances -- however, we can give a "default behavior" by calling e.g. maximum (defValue : xs)
and in some cases that even reads conceptually cleaner than providing an explicit default handler.
As for tail
, I have seldom (never?) encountered a situation where drop 1
was not a strict improvement.
That said, I think tagged warning pragmas seem to be the best path forward here since they seem like something everyone could get behind.
One other thought that briefly came up in discussion at ICFP -- what if along with "warnings" that get attached to functions, we also could have "notice" level notifications? Part of the issue is the idea of a "warning" can signal different things to different people, while a "notice" has perhaps less connotation. For example if we have future plans for some function in base that is not yet deprecated, a "hey! there's future plans, we're telling you now" message is arguably not a warning -- its just a polite heads up.
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..]
.