-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add safe
function to convert partial functions on empty lists to total functions?
#69
Comments
I think it's a bit misleading: |
Somebody asked me on Twitter if the generated code for something like module Example where
safe :: Foldable f => (f a -> b) -> f a -> Maybe b
safe f xs
| null xs = Nothing
| otherwise = Just (f xs)
safeHead :: [a] -> Maybe a
safeHead = safe head $ ghc -O2 -ddump-simpl -dsuppress-all Example.hs
…
safeHead
= \ @ a_a1wg xs_ary ->
case xs_ary of {
[] -> Nothing;
: ds1_a2oE ds2_a2oF -> Just ds1_a2oE
} |
@Bodigrim: The reason I call this function |
yep the name should be changed, not sure what could be the alternative |
On Sun, 29 May 2022, Gabriella Gonzalez wrote:
The idea is to add this function to Data.List:
safe :: ([a] -> b) -> [a] -> Maybe b
safe f [] = Nothing
safe f xs = Just (f xs)
It has nothing to do with safety in the sense of unsafePerformIO or
SafeHaskell. It's about total and partial functions.
I think it is enough to have a function that checks and converts to Maybe
(NonEmpty a) and then apply your NonEmpty functions with 'fmap'.
|
@thielema: The goal is to provide something that is ergonomic. What you are proposing is significantly less ergonomic For example, compare this: safe head [ 1, 2 ] … versus your proposal, which would be: -- Assuming that you were proposing:
-- safe :: [a] -> Maybe (NonEmpty a)
fmap NonEmpty.head (safe [ 1, 2 ]) |
FWIW, that function already exists, |
On Sun, 29 May 2022, Gabriella Gonzalez wrote:
@thielema: The goal is to provide something that is ergonomic. What you
are proposing is significantly less ergonomic
My proposal is total, yours is not.
-- Assuming that you were proposing:
-- safe :: [a] -> Maybe (NonEmpty a)
fmap NonEmpty.head (safe [ 1, 2 ])
NonEmpty.head <$> nonEmpty [1,2]
I think it is better to flag common partial functions with HLint than
pretending that you can make them "safe".
|
This doesn't make sense to me. I can't imagine a situation where it would be better to use |
@thielema @tomjaguarpaw: The problem I'm trying to solve is that right now the path of least resistance is to use the partial functions on lists because:
So I'm not approaching this problem from the standpoint of "there needs to exist a way to compute a safe head". I already know how to do that and it can be done with enough code/imports. Rather, I'm approaching this from the standpoint of "there needs to be a way to compute a safe head that is as ergonomic as the partial head so that the safe version is now the path of least resistance" |
I think that exporting safe versions of all crashing functions in |
@tomjaguarpaw: That was my original idea, but a prior proposal of mine was rejected due to the Fairbairn threshold so this was an attempt to create a proposal that was "Fairbairn threshold"-clean. Specifically, if you have a safeHead = safe head … because (quoting the Wiki page):
|
I would argue that the Fairbairn threshold isn't the only thing that matters here - it seems like a good idea to have a solution that doesn't rely on partial functions, such that you can eliminate them from your code base. |
@JakobBruenker: See my comment here: #69 (comment) The goal here is not to propose that Rather the goal here is to provide an ergonomic total alternative to partial functions to stem the bleeding where people use the partial versions out of convenience. |
[bikeshedding] A name like |
Yeah, I'm not too attached to the name. I'd be fine with alternatives that don't use the word |
+1 to |
I don't really follow the logic. Firstly,
So I don't think I'd like to introduce
(at which point |
I think we just need to rip off the bandaid and remove from the Prelude after a deprecation cycle. The problem isn't that people need help writing total code, but that writing partial code is too often the path of least resistance. Having a |
I see above @Gabriella439 also mentioned the path of least resistance by name too too. I just don't believe adding more things to Prelude can help. |
@tomjaguarpaw: I genuinely do not understand the pushback against this function with respect to safety:
There is no scenario where the |
I find myself sighing a lot when I realize that there is no |
@Profpatch wrote:
It is called |
I downvoted the proposal: |
@andreasabel: Would you approve a different name? |
Here's one approach: {-# language MultiParamTypeClasses #-}
{-# language DataKinds #-}
{-# language TypeFamilies #-}
{-# language TypeApplications #-}
{-# language AllowAmbiguousTypes #-}
{-# language UndecidableInstances #-}
module SillySafe where
import GHC.TypeLits (Symbol)
class May (n :: Symbol) f where
may :: f
instance f ~ ([a] -> Maybe a) => May "head" f where
may [] = Nothing
may (a : _) = Just a
instance f ~ ([a] -> Maybe [a]) => May "tail" f where
may [] = Nothing
may (_ : as) = Just as
instance (f ~ ([a] -> Maybe a), Ord a) => May "maximum" f where
may as = foldr go id as Nothing
where
go x r Nothing = r (Just x)
go x r old@(Just biggest)
| x <= biggest = r old
| otherwise = r (Just x)
-- Examples
may @"head" :: [a] -> Maybe a
may @"tail" :: [a] -> Maybe [a]
may @"maximum" :: Ord a => [a] -> Maybe a I'd much rather use |
I don't believe that it will actually make an observable nudge towards making people write less crashing (more "safe") code in Haskell. I've suggested some alternatives that I think would:
I hope that helps explain my point of view. |
I would like to provide some critical feedback here because one aspect of this process is frustrating for me. Could people please stop proposing wildly different solutions on this thread (such as #69 (comment))? Sorry to single you out @treeowl but you're not the only one. I'm okay with suggesting and discussing related alternatives, including, but not limited to:
However, when people propose radically different solutions on this thread it derails this discussion in a few ways:
More generally, all of these drive-by counter-proposals are unfair to those of us who actually do the work to submit formal proposals and volunteer to implement them. If you have a significantly different counter-proposal that you think would be a better solution, then open an issue to formally propose it. |
Fine. I don't like this proposal because:
|
I'm inclined to agree. It's very hard to judge a proposal out of a broader context which includes a clear problem statement, a description of the status quo, and an analysis of the pros and cons of a wide range of solutions. |
The
I'd be happy with something with a better name, but at that point it's going to be more difficult to explain why you have to use |
I'm okay with the proposal to add |
Also, going back to my comment here: #69 (comment) … please do not propose removing partial functions from
I cannot properly rebut that proposal unless people spell out what they mean by a separate issue to propose the change to remove partial functions from |
As an aside, removing the partial functions might annoy users of Liquid Haskell, who use them quite safely. |
A point of order. Discussing alternative solutions is an important step, especially if a proposal does not cover them. However, a proposer is not obliged to analyze them in full or write detailed rebuttal indeed. A proposer can ask CLC to vote as is. |
Call it |
The proposal has a lot going for it. The naming and ergonomics are a real sweet spot. Let's recall that right now, we have to tell new users that the current name for The alternative namings so far are Yes, usage of safe as a prefix has conventions around SafeHaskell, but that's a pretty minor convention compared with fixing usage of The The argument that we wouldn't do this because it's better to remove partial functions doesn't feel convincing. Even if true in theory, removal will never happen given current community resistance to change. In fact, if you dream of a total prelude, the proposal seems like a blessed pathway. Imagine:
More time goes by, and we arrive at better common practice without harming our archeological heritage. |
Well, it's not quite that nice. You'd need |
How about a modification of the proposal to:
|
Alright, so it seems like the two leading counter-proposals are:
If it's alright with the core libraries committee, I can open up a new issue for (A) and if that issue is approved then I can close this one. I'm asking because I don't know if that's easier or more difficult for them to review it as an independent issue. Otherwise we can keep that discussion here. For (B), the name I would suggest to use instead of For example, then it's clear from the name that |
Pinging @mstksg of |
@mixphix: Can you clarify what you are proposing? Specifically, how would you implement the safe head function under your proposal? |
Could you provide the proposed haddock for your new function, along with fixing on a name at least for now? ITBH 'm instinctively against this proposal even regardless of whether any alternative is implemented, because I feel the new function will still be confusing to use and not really that useful. However the haddock could convince me otherwise. |
I'll tentatively propose Also, here are the following sample haddocks like @hsenag requests: {-| Use `nullSafe` to wrap a partial function on lists (or, more generally, `Foldable`
collections) if that function fails on an empty (`null`) input. Examples of partial
functions that benefit from being wrapped in this way are:
* `head`
* `tail`
* `foldr1`
* `maximum`
For example:
>>> head [2,3,5]
2
>>> head []
*** Exception: Prelude.head: empty list
>>> nullSafe head [2,3,5]
Just 2
>>> nullSafe head []
Nothing
This function does not fix all partial functions. In particular,
this does not fix functions that fail on non-`null` inputs. For
example, the `tail . tail` function fails on both an empty input
and a 1-element input:
>>> (tail . tail) [2,3,5]
[5]
>>> (tail. tail) [2]
*** Exception: Prelude.tail: empty list
>>> (tail . tail) []
*** Exception: Prelude.tail: empty list
… but if you were to wrap that in `nullSafe` then that would
only fix the function for an empty input:
>>> nullSafe (tail . tail) [2,3,5]
Just [5]
Prelude> nullSafe (tail . tail) [2]
Just *** Exception: Prelude.tail: empty list
Prelude> nullSafe (tail . tail) []
Nothing
The mnemonic is that a @`nullSafe` f@ is a \"null-safe f\" (e.g.
\"null-safe head\" or \"null-safe tail\").
-}
nullSafe :: Foldable f => (f a -> b) -> f a -> Maybe b
nullSafe f xs
| null xs = Nothing
| otherwise = Just (f xs) |
Thanks - I think the name and the haddocks are quite clear. I've thought about my general objection a bit more and I think it's really that I don't see much point in |
The CPSed version of this is ifNull :: Foldable f => f a -> b -> (f a -> b) -> b
ifNull xs b f
| null xs = b
| otherwise = f xs
nullSafe f xs = ifNull xs Nothing (Just . f)
ifNull xs b f = fromMaybe b $ nullSafe f xs My tendency would be to go with the CPSed But also, nullSafe :: (Eq a, Monoid a) => (a -> b) -> a -> Maybe b
nullSafe f a
| a == mempty = Nothing
| otherwise = Just $ f a Both I am still not convinced of this proposal. |
@hsenag: You can pattern match on the list instead of using |
On Thu, 2 Jun 2022, Gabriella Gonzalez wrote:
@hsenag: You can pattern match on the list instead of using safeHead /
safeTail, but that doesn't work as well for other partial functions like
init or last
I use a function viewR that allows me to pattern match on the end of a
list:
https://hackage.haskell.org/package/utility-ht-0.0.16/docs/Data-List-HT.html#v:viewR
It analogous to the one in Data.Sequence
https://hackage.haskell.org/package/containers-0.6.5.1/docs/Data-Sequence.html#v:viewr
|
What about |
If it's alright with the committee, I'd like to pause this until #70 is reviewed since if that were accepted I would prefer that over my own proposal |
I'd rather just deprecate partiality in |
@Gabriella439 how would you like to proceed with this, given that #70 is blocked on a GHC proposal and #87 has been approved? |
I'm fine with closing this |
The idea is to add this function to
Data.List
:… which you can use like this:
… and so on. Another possible variation on this proposal is to define
safe
to work onFoldable
s instead of monomorphic lists:… in which case it would probably go in
Data.Foldable
instead ofData.List
.The text was updated successfully, but these errors were encountered: