Skip to content

Match nub* functions with Array #179

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

Merged
merged 24 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

- uses: purescript-contrib/setup-purescript@main
with:
purescript: "0.14.0-rc3"
purescript: "0.14.0-rc5"

- uses: actions/setup-node@v1
with:
Expand Down
136 changes: 123 additions & 13 deletions src/Data/List.purs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ module Data.List

, nub
, nubBy
, nubEq
, nubByEq
, union
, unionBy
, delete
Expand All @@ -101,22 +103,21 @@ import Control.Alt ((<|>))
import Control.Alternative (class Alternative)
import Control.Lazy (class Lazy, defer)
import Control.Monad.Rec.Class (class MonadRec, Step(..), tailRecM, tailRecM2)

import Data.Bifunctor (bimap)
import Data.Foldable (class Foldable, foldr, any, foldl)
import Data.Foldable (foldl, foldr, foldMap, fold, intercalate, elem, notElem, find, findMap, any, all) as Exports
import Data.Function (on)
import Data.FunctorWithIndex (mapWithIndex) as FWI
import Data.List.Types (List(..), (:))
import Data.List.Types (NonEmptyList(..)) as NEL
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.NonEmpty ((:|))
import Data.Traversable (scanl, scanr) as Exports
import Data.Traversable (sequence)
import Data.Tuple (Tuple(..))
import Data.Tuple (Tuple(..), fst, snd)
import Data.Unfoldable (class Unfoldable, unfoldr)

import Data.Foldable (foldl, foldr, foldMap, fold, intercalate, elem, notElem, find, findMap, any, all) as Exports
import Data.Traversable (scanl, scanr) as Exports

import Prim.TypeError (class Warn, Text)

-- | Convert a list into any unfoldable structure.
Expand Down Expand Up @@ -663,18 +664,68 @@ tails list@(Cons _ tl)= list : tails tl
--------------------------------------------------------------------------------

-- | Remove duplicate elements from a list.
-- | Keeps the first occurrence of each element in the input list,
-- | in the same order they appear in the input list.
-- |
-- | ```purescript
-- | nub 1:2:1:3:3:Nil == 1:2:3:Nil
-- | ```
-- |
-- | Running time: `O(n log n)`
nub :: forall a. Ord a => List a -> List a
nub = nubBy compare

-- | Remove duplicate elements from a list based on the provided comparison function.
-- | Keeps the first occurrence of each element in the input list,
-- | in the same order they appear in the input list.
-- |
-- | ```purescript
-- | nubBy (compare `on` Array.length) ([1]:[2]:[3,4]:Nil) == [1]:[3,4]:Nil
-- | ```
-- |
-- | Running time: `O(n log n)`
nubBy :: forall a. (a -> a -> Ordering) -> List a -> List a
nubBy p =
-- Discard indices, just keep original values.
mapReverse snd
-- Sort by index to recover original order.
-- Use `flip` to sort in reverse order in anticipation of final `mapReverse`.
<<< sortBy (flip compare `on` fst)
-- Removing neighboring duplicates.
<<< nubByAdjacentReverse (\a b -> (p `on` snd) a b == EQ)
-- Sort by original values to cluster duplicates.
<<< sortBy (p `on` snd)
-- Add indices so we can recover original order after deduplicating.
<<< addIndexReverse

-- | Remove duplicate elements from a list.
-- | Keeps the first occurrence of each element in the input list,
-- | in the same order they appear in the input list.
-- | This less efficient version of `nub` only requires an `Eq` instance.
-- |
-- | ```purescript
-- | nubEq 1:2:1:3:3:Nil == 1:2:3:Nil
-- | ```
-- |
-- | Running time: `O(n^2)`
nub :: forall a. Eq a => List a -> List a
nub = nubBy eq
nubEq :: forall a. Eq a => List a -> List a
nubEq = nubByEq eq

-- | Remove duplicate elements from a list, using the specified
-- | function to determine equality of elements.
-- | Remove duplicate elements from a list, using the provided equivalence function.
-- | Keeps the first occurrence of each element in the input list,
-- | in the same order they appear in the input list.
-- | This less efficient version of `nubBy` only requires an equivalence
-- | function, rather than an ordering function.
-- |
-- | ```purescript
-- | mod3eq = eq `on` \n -> mod n 3
-- | nubByEq mod3eq 1:3:4:5:6:Nil == 1:3:5:Nil
-- | ```
-- |
-- | Running time: `O(n^2)`
nubBy :: forall a. (a -> a -> Boolean) -> List a -> List a
nubBy _ Nil = Nil
nubBy eq' (x : xs) = x : nubBy eq' (filter (\y -> not (eq' x y)) xs)
nubByEq :: forall a. (a -> a -> Boolean) -> List a -> List a
nubByEq _ Nil = Nil
nubByEq eq' (x : xs) = x : nubByEq eq' (filter (\y -> not (eq' x y)) xs)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was never stack-safe. Reported in #194


-- | Calculate the union of two lists.
-- |
Expand All @@ -687,7 +738,7 @@ union = unionBy (==)
-- |
-- | Running time: `O(n^2)`
unionBy :: forall a. (a -> a -> Boolean) -> List a -> List a -> List a
unionBy eq xs ys = xs <> foldl (flip (deleteBy eq)) (nubBy eq ys) xs
unionBy eq xs ys = xs <> foldl (flip (deleteBy eq)) (nubByEq eq ys) xs

-- | Delete the first occurrence of an element from a list.
-- |
Expand Down Expand Up @@ -794,3 +845,62 @@ transpose ((x : xs) : xss) =
foldM :: forall m a b. Monad m => (b -> a -> m b) -> b -> List a -> m b
foldM _ b Nil = pure b
foldM f b (a : as) = f b a >>= \b' -> foldM f b' as

--------------------------------------------------------------------------------
-- Fast operations which also reverse the list ---------------------------------
--------------------------------------------------------------------------------

-- | Maps a function to each element in a list
-- | and reverses the result, but faster than
-- | running each separately. Equivalent to:
-- |
-- | ```purescript
-- | \f l = map f l # reverse
-- | ```
-- |
-- | Running time: `O(n)`
mapReverse :: forall a b. (a -> b) -> List a -> List b
mapReverse f = go Nil
where
go :: List b -> List a -> List b
go acc Nil = acc
go acc (x : xs) = go (f x : acc) xs

-- | Converts each element to a Tuple containing its index,
-- | and reverses the result, but faster than running separately.
-- | Equivalent to:
-- |
-- | ```purescript
-- | reverse <<< mapWithIndex Tuple
-- | ```
-- |
-- | Running time: `O(n)`
addIndexReverse :: forall a. List a -> List (Tuple Int a)
addIndexReverse = go 0 Nil
where
go :: Int -> List (Tuple Int a) -> List a -> List (Tuple Int a)
go i acc Nil = acc
go i acc (x : xs) = go (i + 1) ((Tuple i x) : acc) xs

-- | Removes neighboring duplicate items from a list
-- | based on an equality predicate.
-- | Keeps the LAST element if duplicates are encountered.
-- | Returned list is reversed (this is to improve performance).
-- |
-- | ```purescript
-- | nubByAdjacentReverse (on eq length) ([1]:[2]:[3,4]:Nil) == [3,4]:[2]:Nil`
-- | ```
-- |
-- | Running time: `O(n)`
nubByAdjacentReverse :: forall a. (a -> a -> Boolean) -> List a -> List a
nubByAdjacentReverse p = go Nil
where
go :: List a -> List a -> List a
-- empty output
go Nil (x : xs) = go (x : Nil) xs
-- checking for duplicates
go acc@(a : as) (x : xs)
| p a x = go (x : as) xs
| otherwise = go (x : acc) xs
-- empty input
go acc Nil = acc
16 changes: 8 additions & 8 deletions src/Data/List/Lazy.purs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ module Data.List.Lazy
, groupBy
, partition

, nub
, nubBy
, nubEq
, nubByEq
, union
, unionBy
, delete
Expand Down Expand Up @@ -593,18 +593,18 @@ partition f = foldr go {yes: nil, no: nil}
-- | Remove duplicate elements from a list.
-- |
-- | Running time: `O(n^2)`
nub :: forall a. Eq a => List a -> List a
nub = nubBy eq
nubEq :: forall a. Eq a => List a -> List a
nubEq = nubByEq eq

-- | Remove duplicate elements from a list, using the specified
-- | function to determine equality of elements.
-- |
-- | Running time: `O(n^2)`
nubBy :: forall a. (a -> a -> Boolean) -> List a -> List a
nubBy eq = List <<< map go <<< unwrap
nubByEq :: forall a. (a -> a -> Boolean) -> List a -> List a
nubByEq eq = List <<< map go <<< unwrap
where
go Nil = Nil
go (Cons x xs) = Cons x (nubBy eq (filter (\y -> not (eq x y)) xs))
go (Cons x xs) = Cons x (nubByEq eq (filter (\y -> not (eq x y)) xs))

-- | Calculate the union of two lists.
-- |
Expand All @@ -617,7 +617,7 @@ union = unionBy (==)
-- |
-- | Running time: `O(n^2)`
unionBy :: forall a. (a -> a -> Boolean) -> List a -> List a -> List a
unionBy eq xs ys = xs <> foldl (flip (deleteBy eq)) (nubBy eq ys) xs
unionBy eq xs ys = xs <> foldl (flip (deleteBy eq)) (nubByEq eq ys) xs

-- | Delete the first occurrence of an element from a list.
-- |
Expand Down
12 changes: 10 additions & 2 deletions src/Data/List/NonEmpty.purs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ module Data.List.NonEmpty
, partition
, nub
, nubBy
, nubEq
, nubByEq
, union
, unionBy
, intersect
Expand Down Expand Up @@ -278,12 +280,18 @@ groupAllBy = wrappedOperation "groupAllBy" <<< L.groupAllBy
partition :: forall a. (a -> Boolean) -> NonEmptyList a -> { yes :: L.List a, no :: L.List a }
partition = lift <<< L.partition

nub :: forall a. Eq a => NonEmptyList a -> NonEmptyList a
nub :: forall a. Ord a => NonEmptyList a -> NonEmptyList a
nub = wrappedOperation "nub" L.nub

nubBy :: forall a. (a -> a -> Boolean) -> NonEmptyList a -> NonEmptyList a
nubBy :: forall a. (a -> a -> Ordering) -> NonEmptyList a -> NonEmptyList a
nubBy = wrappedOperation "nubBy" <<< L.nubBy

nubEq :: forall a. Eq a => NonEmptyList a -> NonEmptyList a
nubEq = wrappedOperation "nubEq" L.nubEq

nubByEq :: forall a. (a -> a -> Boolean) -> NonEmptyList a -> NonEmptyList a
nubByEq = wrappedOperation "nubByEq" <<< L.nubByEq

union :: forall a. Eq a => NonEmptyList a -> NonEmptyList a -> NonEmptyList a
union = wrappedOperation2 "union" L.union

Expand Down
15 changes: 12 additions & 3 deletions test/Test/Data/List.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ module Test.Data.List (testList) where

import Prelude

import Data.Array as Array
import Data.Foldable (foldMap, foldl)
import Data.FoldableWithIndex (foldMapWithIndex, foldlWithIndex, foldrWithIndex)
import Data.List (List(..), (..), stripPrefix, Pattern(..), length, range, foldM, unzip, zip, zipWithA, zipWith, intersectBy, intersect, (\\), deleteBy, delete, unionBy, union, nubBy, nub, group, groupAll, groupBy, groupAllBy, partition, span, dropWhile, drop, dropEnd, takeWhile, take, takeEnd, sortBy, sort, catMaybes, mapMaybe, filterM, filter, concat, concatMap, reverse, alterAt, modifyAt, updateAt, deleteAt, insertAt, findLastIndex, findIndex, elemLastIndex, elemIndex, (!!), uncons, unsnoc, init, tail, last, head, insertBy, insert, snoc, null, singleton, fromFoldable, transpose, mapWithIndex, (:))
import Data.Function (on)
import Data.List (List(..), Pattern(..), alterAt, catMaybes, concat, concatMap, delete, deleteAt, deleteBy, drop, dropEnd, dropWhile, elemIndex, elemLastIndex, filter, filterM, findIndex, findLastIndex, foldM, fromFoldable, group, groupAll, groupAllBy, groupBy, head, init, insert, insertAt, insertBy, intersect, intersectBy, last, length, mapMaybe, mapWithIndex, modifyAt, nub, nubBy, nubByEq, nubEq, null, partition, range, reverse, singleton, snoc, sort, sortBy, span, stripPrefix, tail, take, takeEnd, takeWhile, transpose, uncons, union, unionBy, unsnoc, unzip, updateAt, zip, zipWith, zipWithA, (!!), (..), (:), (\\))
import Data.List.NonEmpty as NEL
import Data.Maybe (Maybe(..), isNothing, fromJust)
import Data.Monoid.Additive (Additive(..))
Expand Down Expand Up @@ -285,8 +287,15 @@ testList = do
assert $ nub (l [1, 2, 2, 3, 4, 1]) == l [1, 2, 3, 4]

log "nubBy should remove duplicate items from the list using a supplied predicate"
let nubPred = \x y -> if odd x then false else x == y
assert $ nubBy nubPred (l [1, 2, 2, 3, 3, 4, 4, 1]) == l [1, 2, 3, 3, 4, 1]
let nubPred = compare `on` Array.length
assert $ nubBy nubPred (l [[1],[2],[3,4]]) == l [[1],[3,4]]

log "nubEq should remove duplicate elements from the list, keeping the first occurence"
assert $ nubEq (l [1, 2, 2, 3, 4, 1]) == l [1, 2, 3, 4]

log "nubByEq should remove duplicate items from the list using a supplied predicate"
let mod3eq = eq `on` \n -> mod n 3
assert $ nubByEq mod3eq (l [1, 3, 4, 5, 6]) == l [1, 3, 5]

log "union should produce the union of two lists"
assert $ union (l [1, 2, 3]) (l [2, 3, 4]) == l [1, 2, 3, 4]
Expand Down
10 changes: 5 additions & 5 deletions test/Test/Data/List/Lazy.purs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Control.Lazy (defer)
import Data.FoldableWithIndex (foldMapWithIndex, foldlWithIndex, foldrWithIndex)
import Data.FunctorWithIndex (mapWithIndex)
import Data.Lazy as Z
import Data.List.Lazy (List, Pattern(..), alterAt, catMaybes, concat, concatMap, cons, delete, deleteAt, deleteBy, drop, dropWhile, elemIndex, elemLastIndex, filter, filterM, findIndex, findLastIndex, foldM, foldMap, foldl, foldr, foldrLazy, fromFoldable, group, groupBy, head, init, insert, insertAt, insertBy, intersect, intersectBy, iterate, last, length, mapMaybe, modifyAt, nil, nub, nubBy, null, partition, range, repeat, replicate, replicateM, reverse, scanlLazy, singleton, slice, snoc, span, stripPrefix, tail, take, takeWhile, transpose, uncons, union, unionBy, unzip, updateAt, zip, zipWith, zipWithA, (!!), (..), (:), (\\))
import Data.List.Lazy (List, Pattern(..), alterAt, catMaybes, concat, concatMap, cons, delete, deleteAt, deleteBy, drop, dropWhile, elemIndex, elemLastIndex, filter, filterM, findIndex, findLastIndex, foldM, foldMap, foldl, foldr, foldrLazy, fromFoldable, group, groupBy, head, init, insert, insertAt, insertBy, intersect, intersectBy, iterate, last, length, mapMaybe, modifyAt, nil, nubEq, nubByEq, null, partition, range, repeat, replicate, replicateM, reverse, scanlLazy, singleton, slice, snoc, span, stripPrefix, tail, take, takeWhile, transpose, uncons, union, unionBy, unzip, updateAt, zip, zipWith, zipWithA, (!!), (..), (:), (\\))
import Data.List.Lazy.NonEmpty as NEL
import Data.Maybe (Maybe(..), isNothing, fromJust)
import Data.Monoid.Additive (Additive(..))
Expand Down Expand Up @@ -328,12 +328,12 @@ testListLazy = do
log "iterate on nonempty lazy list should apply supplied function correctly"
assert $ (take 3 $ NEL.toList $ NEL.iterate (_ + 1) 0) == l [0, 1, 2]

log "nub should remove duplicate elements from the list, keeping the first occurence"
assert $ nub (l [1, 2, 2, 3, 4, 1]) == l [1, 2, 3, 4]
log "nubEq should remove duplicate elements from the list, keeping the first occurence"
assert $ nubEq (l [1, 2, 2, 3, 4, 1]) == l [1, 2, 3, 4]

log "nubBy should remove duplicate items from the list using a supplied predicate"
log "nubByEq should remove duplicate items from the list using a supplied predicate"
let nubPred = \x y -> if odd x then false else x == y
assert $ nubBy nubPred (l [1, 2, 2, 3, 3, 4, 4, 1]) == l [1, 2, 3, 3, 4, 1]
assert $ nubByEq nubPred (l [1, 2, 2, 3, 3, 4, 4, 1]) == l [1, 2, 3, 3, 4, 1]

log "union should produce the union of two lists"
assert $ union (l [1, 2, 3]) (l [2, 3, 4]) == l [1, 2, 3, 4]
Expand Down
13 changes: 11 additions & 2 deletions test/Test/Data/List/NonEmpty.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ module Test.Data.List.NonEmpty (testNonEmptyList) where

import Prelude

import Data.Array as Array
import Data.Foldable (class Foldable, foldM, foldMap, foldl, length)
import Data.FoldableWithIndex (foldlWithIndex, foldrWithIndex, foldMapWithIndex)
import Data.Function (on)
import Data.List as L
import Data.List.NonEmpty as NEL
import Data.Maybe (Maybe(..))
Expand Down Expand Up @@ -186,8 +188,15 @@ testNonEmptyList = do
assert $ NEL.nub (nel 1 [2, 2, 3, 4, 1]) == nel 1 [2, 3, 4]

log "nubBy should remove duplicate items from the list using a supplied predicate"
let nubPred = \x y -> if odd x then false else x == y
assert $ NEL.nubBy nubPred (nel 1 [2, 2, 3, 3, 4, 4, 1]) == nel 1 [2, 3, 3, 4, 1]
let nubPred = compare `on` Array.length
assert $ NEL.nubBy nubPred (nel [1] [[2],[3,4]]) == nel [1] [[3,4]]

log "nubEq should remove duplicate elements from the list, keeping the first occurence"
assert $ NEL.nubEq (nel 1 [2, 2, 3, 4, 1]) == nel 1 [2, 3, 4]

log "nubByEq should remove duplicate items from the list using a supplied predicate"
let mod3eq = eq `on` \n -> mod n 3
assert $ NEL.nubByEq mod3eq (nel 1 [3, 4, 5, 6]) == nel 1 [3, 5]

log "union should produce the union of two lists"
assert $ NEL.union (nel 1 [2, 3]) (nel 2 [3, 4]) == nel 1 [2, 3, 4]
Expand Down