haskell / core-libraries-committee

95 stars 15 forks source link

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

Closed Bodigrim closed 10 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

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

brandonchinn178 commented 1 year ago

Why only head and tail? Because these are functions, for which safe concise drop-in replacements exist. 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.

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

cdsmith commented 1 year ago

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.

treeowl commented 1 year ago

How would this affect users of Liquid Haskell?

josephcsible commented 1 year ago

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.

ocramz commented 1 year ago

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!

adamgundry commented 1 year ago

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

chshersh commented 1 year ago

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" #-}
treeowl commented 1 year ago

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.

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

googleson78 commented 1 year ago

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

ElvishJerricco commented 1 year ago

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.

  1. 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.
  2. The warning isn't providing any information. It's not warning you about a mistake you may have missed. You know you used head or tail.
  3. Warnings generally shouldn't be complaints about coding style and they shouldn't be asking you to make fundamental changes to how you've structured your code. They should assume you generally know what you're doing and that you just need some help with things that are commonly missed. Using 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.

tomjaguarpaw commented 1 year ago

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.

ParetoOptimalDev commented 1 year ago

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.

avanov commented 1 year ago

This will negatively affect those who use -Werror seriously

tomjaguarpaw commented 1 year ago

This will negatively affect those who use -Werror seriously

Do you mean "this will negatively affect those who use -Werror and head/tail seriously"?

avanov commented 1 year ago

Well yes, and those who use Liquid Haskell too.

tomjaguarpaw commented 1 year ago

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.

avanov commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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.

avanov commented 1 year ago

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.

tomjaguarpaw commented 1 year ago

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.

avanov commented 1 year ago

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.

chshersh commented 1 year ago

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

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.

tomjaguarpaw commented 1 year ago

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

Icelandjack commented 1 year ago

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.

nuttycom commented 1 year ago

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.

ParetoOptimalDev commented 1 year ago

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

josephcsible commented 1 year ago

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.

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

Ericson2314 commented 1 year ago

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

Ericson2314 commented 1 year ago

FWIW I do support this one too.

billksun commented 1 year ago

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?

tavrinky commented 1 year ago

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.

brandonchinn178 commented 1 year ago

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.

tavrinky commented 1 year ago

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.

Bodigrim commented 1 year ago

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.

Ericson2314 commented 1 year ago

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

brandonchinn178 commented 1 year ago

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')
avanov commented 1 year ago

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? ;)

ParetoOptimalDev commented 1 year ago

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.

avanov commented 1 year ago

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.

jberryman commented 1 year ago

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.

Can I offer an alternative proposal?:

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)

simonmar commented 1 year ago

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.

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

googleson78 commented 1 year ago

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

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.

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

avanov commented 1 year ago

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.

Boarders commented 1 year ago

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

chshersh commented 1 year ago

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

ocharles commented 1 year ago

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:

chshersh commented 1 year ago

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