haskell / core-libraries-committee

95 stars 15 forks source link

Add {-# WARNING #-} to Data.List.{head,tail} #87

Closed Bodigrim closed 11 months ago

Bodigrim commented 1 year ago

Haddocks for Data.List (technically, GHC.List) warn against head and tail on the ground of their partiality.

I propose to promote these warnings to the pragma level as per MR !9290:

{-# WARNING head "This is a partial function, it throws an error on empty lists. Use pattern matching or Data.List.uncons instead. Consider refactoring to use Data.List.NonEmpty." #-}
{-# WARNING tail "This is a partial function, it throws an error on empty lists. Replace it with drop 1, or use pattern matching or Data.List.uncons instead. Consider refactoring to use Data.List.NonEmpty." #-}

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 and tail? 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., for init / last there is currently no such replacement (one must push for addition of Data.List.unsnoc first), and things like !! and maximum 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 in base 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 and tail 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 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, containing original definitions of head and tail without {-# WARNING #-}. I'm however opposed to introducing such Data.List.Partial into base 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 [] without head and tail?

instance MonadFix [] where
    mfix f = case fix (f . head) of
               []    -> []
               (x:_) -> x : mfix (tail . f)

I'd rewrite it this way:

instance MonadFix [] where
    mfix f = case fix (take 1 >=> f) of
               []    -> []
               (x:_) -> x : mfix (drop 1 . f)

How would you rewrite this snippet?

case product xs of
  1 -> foo
  n -> bar n (head xs)

Besides options in https://github.com/haskell/core-libraries-committee/issues/87#issuecomment-1243587955, one can do this:

case (xs, product xs) of
  ([], _)    -> foo
  (_, 1)     -> foo
  (x : _, n) -> bar n x

or (if you insist on exactly two clauses):

case xs of
  x : _ | n <- product xs, n /= 1 = bar n x
  _ -> foo

How would you rewrite this snippet?

head $ filter (`notElem` hashes) $ map showt [0::Int ..]

I'd use a proper library for infinite lists aka streams: Stream, streams or infinite-list. E. g., Stream provides total head :: Stream a -> a and filter :: Stream a -> Stream a, so the snippet can be rewritten in a total way as

import Data.Stream as S 
S.head $ S.filter (`notElem` hashes) $ S.map showt $ S.iterate (+1) (0 :: Int)

infinite-list can make it even neater offering (0...) syntax to replace [0..].

Boarders commented 1 year 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.

tomjaguarpaw commented 1 year ago

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)

We have only so much labour available. Who is going to write this putative documentation or tooling?

simonmar commented 1 year ago

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?

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.

tomjaguarpaw commented 1 year ago

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?

chshersh commented 1 year ago

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".

Innf107 commented 1 year ago

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.

mixphix commented 1 year ago

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?

tomjaguarpaw commented 1 year ago

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.

googleson78 commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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?

marcosh commented 1 year ago

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

tomjaguarpaw commented 1 year ago

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.

simonmar commented 1 year ago

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.

simonmar commented 1 year ago

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.

parsonsmatt commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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!

tomjaguarpaw commented 1 year ago

@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.

Boarders commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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.

mixphix commented 1 year ago
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.

Boarders commented 1 year ago

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.

mixphix commented 1 year ago

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.

konsumlamm commented 1 year ago

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.

Boarders commented 1 year ago

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.

ocharles commented 1 year ago

functions that may violate type safety

Isn't that literally what head :: [a] -> a is doing?

tomjaguarpaw commented 1 year ago

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.

konsumlamm commented 1 year ago

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.

ocharles commented 1 year ago

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.

simonmar commented 1 year ago

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.

simonmar commented 1 year ago

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?

parsonsmatt commented 1 year ago

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.

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.

tomjaguarpaw commented 1 year ago

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.

Ericson2314 commented 1 year ago

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.

konsumlamm commented 1 year ago

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).

tomjaguarpaw commented 1 year ago

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!

simonmar commented 1 year ago

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.

Does that seem plausible?

nomeata commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

@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?

tomjaguarpaw commented 1 year ago

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?

tomjaguarpaw commented 1 year ago

Discussion summary

Commenter's notes

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.

Arguments in favour

Arguments against

simonmar commented 1 year ago

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.)

tomjaguarpaw commented 1 year ago

[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):

On the other hand I'm not, per se, against:

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.

Boarders commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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.

cdsmith commented 1 year ago

About partial functions

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.

About warnings

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.

gbaz commented 1 year ago

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 :-)

ParetoOptimalDev commented 1 year ago

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?

tomjaguarpaw commented 1 year ago

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.

Ericson2314 commented 1 year ago

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.

gbaz commented 1 year ago

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.