Skip to content

Hlint: suggest nub alternative#11825

Open
philderbeast wants to merge 7 commits into
haskell:masterfrom
cabalism:hlint/suggest-nub-alternatives
Open

Hlint: suggest nub alternative#11825
philderbeast wants to merge 7 commits into
haskell:masterfrom
cabalism:hlint/suggest-nub-alternatives

Conversation

@philderbeast
Copy link
Copy Markdown
Collaborator

@philderbeast philderbeast commented May 14, 2026

Fixes #11824.

When fired, this suggestion renders as:

$ cabal-install-solver/src/Distribution/Solver/Modular/Assignment.hs:56:64-69: Suggestion: Use ordNub
Found:
  nub xs
Perhaps:
  ordNub xs
Note: The growth rate of nub is O(n^2) and for ordNub it is O(n log n).  May require adding import of either;
  - Distribution.Utils.Generic from Cabal-syntax, or
  - Distribution.Simple.Utils from Cabal.

I didn't correct O(n log n) to the O(n log d) from containers haddocks for nubOrd. Neither did I suggest using that equivalent function from containers. For that, I raised #11826.

I had to add Ord instances to a few data types, BuildTarget and MatchError. I changed the Eq constraint to an Ord constraint for findMatch and nubMatches.

I left a note in .hlint.yaml to only use single letter variables in rules.

I used a separate commit for changes to tests but on approval will squash all commits before adding the merge me label.


  • Patches conform to the coding conventions.
  • Is this a PR that fixes CI? If so, it will need to be backported to older cabal release branches (ask maintainers for directions).

Copy link
Copy Markdown
Collaborator

@jappeace jappeace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

| n <-
nub $
ordNub $
sort $
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the sort no longer needed

Copy link
Copy Markdown
Collaborator

@Bodigrim Bodigrim May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite: ordNub does not change the order of elements, so it's not semantically equivalent to drop sort. (I don't know whether sorting makes any difference for the purposes of this function and elsewhere, but perhaps we can leave figuring this out for another time)


uniqueNames :: Bool
uniqueNames = length allExplanationIdStrings == length (nub allExplanationIdStrings)
uniqueNames = length allExplanationIdStrings == length (ordNub allExplanationIdStrings)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a regression, but this is wasteful. Would be nice to have a helper areUnique :: Ord a => [a] -> Bool, there is at least one more use case for it above.

Copy link
Copy Markdown
Collaborator Author

@philderbeast philderbeast May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please be more specific?

-- Like `uniqueNames` in the way it is implemented but taking a `[a]`.
areUnique :: Ord a => [a] -> Bool
areUnique xs = length xs == length (ordNub xs)

I went looking in this repository for other [a] -> Bool helper functions and there are a few, including these ones with the same signature:

hasNoDups :: Ord a => [a] -> Bool
hasNoDups = loop Set.empty
where
loop _ [] = True
loop s (x:xs) | s' <- Set.insert x s, Set.size s' > Set.size s
= loop s' xs
| otherwise
= False

listUnique :: Ord a => [a] -> Bool
listUnique xs =
let sorted = sort xs
in nub sorted == xs

isSorted :: Ord a => [a] -> Bool
isSorted (x1 : xs@(x2 : _)) = x1 <= x2 && isSorted xs
isSorted _ = True

Copy link
Copy Markdown
Collaborator Author

@philderbeast philderbeast May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also found these two doing the same thing, the opposite of all items being unique, all items being duplicates:

allEqual :: Eq a => [a] -> Bool
allEqual xs = length (nub xs) == 1

allEqual :: Eq a => [a] -> Bool
allEqual [] = True
allEqual [_] = True
allEqual (x:y:ys) = x == y && allEqual (y:ys)

Other implementations found hoogling:

allEqual :: Eq a => [a] -> Bool
allEqual []       = True
allEqual (x : xs) = all (== x) xs
allSame :: Eq a => [a] -> Bool
allSame [] = True
allSame (x:xs) = all (x ==) xs

Copy link
Copy Markdown
Collaborator

@Bodigrim Bodigrim May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that areUnique xs = length xs == length (ordNub xs) is a very expensive implementation. It is better than the one using nub, but still not great. You would expect such function to return False as soon as it meets the first non-unique element, but the implementation has to build ordNub xs in full until being able to compare lengths.

It should be something like

isUnique :: [a] -> Bool 
isUnique = go S.empty 
  where 
    go !_ [] = True 
    go seen (x : xs) = if S.member x seen then False else go (S.insert x seen) xs

allEqual expressed via nub also has bad performance, it should be like Agda's implementation.

Up to you whether you want to fix these things in this PR, it's indeed a bit of scope creep.

| -- | A specific file within a specific component.
BuildTargetFile ComponentName FilePath
deriving (Eq, Show, Generic)
deriving (Eq, Ord, Show, Generic)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this deserves @since. Same below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an hlint rule not to use nub

3 participants