Skip to content

Add short circuiting functions of elem, notElem, find, findMap, scanl, and scanr #189

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 12 commits into from
Dec 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions src/Data/Array.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ exports.indexImpl = function (just) {
};
};

exports.findMapImpl = function (nothing) {
return function (isJust) {
return function (f) {
return function (xs) {
for (var i = 0; i < xs.length; i++) {
var result = f(xs[i]);
if (isJust(result)) return result;
}
return nothing;
};
};
};
};

exports.findIndexImpl = function (just) {
return function (nothing) {
return function (f) {
Expand Down Expand Up @@ -222,6 +236,36 @@ exports.partition = function (f) {
};
};

exports.scanl = function (f) {
return function (b) {
return function (xs) {
var len = xs.length;
var acc = b;
var out = new Array(len);
for (var i = 0; i < len; i++) {
acc = f(acc)(xs[i]);
out[i] = acc;
}
return out;
};
};
};

exports.scanr = function (f) {
return function (b) {
return function (xs) {
var len = xs.length;
var acc = b;
var out = new Array(len);
for (var i = len - 1; i >= 0; i--) {
acc = f(xs[i])(acc);
out[i] = acc;
}
return out;
};
};
};

//------------------------------------------------------------------------------
// Sorting ---------------------------------------------------------------------
//------------------------------------------------------------------------------
Expand Down
61 changes: 58 additions & 3 deletions src/Data/Array.purs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ module Data.Array
, unsnoc

, (!!), index
, elem
, notElem
, elemIndex
, elemLastIndex
, find
, findMap
, findIndex
, findLastIndex
, insertAt
Expand All @@ -74,6 +78,8 @@ module Data.Array
, mapMaybe
, catMaybes
, mapWithIndex
, scanl
, scanr

, sort
, sortBy
Expand Down Expand Up @@ -127,9 +133,8 @@ import Data.Array.NonEmpty.Internal (NonEmptyArray(..))
import Data.Array.ST as STA
import Data.Array.ST.Iterator as STAI
import Data.Foldable (class Foldable, foldl, foldr, traverse_)
import Data.Foldable (foldl, foldr, foldMap, fold, intercalate, elem, notElem, find, findMap, any, all) as Exports
import Data.Maybe (Maybe(..), maybe, isJust, fromJust)
import Data.Traversable (scanl, scanr) as Exports
import Data.Foldable (foldl, foldr, foldMap, fold, intercalate, any, all) as Exports
import Data.Maybe (Maybe(..), maybe, isJust, fromJust, isNothing)
import Data.Traversable (sequence, traverse)
import Data.Tuple (Tuple(..), fst, snd)
import Data.Unfoldable (class Unfoldable, unfoldr)
Expand Down Expand Up @@ -398,6 +403,14 @@ foreign import indexImpl
-- |
infixl 8 index as !!

-- | Returns true if the array has the given element.
elem :: forall a. Eq a => a -> Array a -> Boolean
elem a arr = isJust $ elemIndex a arr

-- | Returns true if the array does not have the given element.
notElem :: forall a. Eq a => a -> Array a -> Boolean
notElem a arr = isNothing $ elemIndex a arr

-- | Find the index of the first element equal to the specified element.
-- |
-- | ```purescript
Expand All @@ -418,6 +431,28 @@ elemIndex x = findIndex (_ == x)
elemLastIndex :: forall a. Eq a => a -> Array a -> Maybe Int
elemLastIndex x = findLastIndex (_ == x)

-- | Find the first element for which a predicate holds.
-- |
-- | ```purescript
-- | findIndex (contains $ Pattern "b") ["a", "bb", "b", "d"] = Just "bb"
-- | findIndex (contains $ Pattern "x") ["a", "bb", "b", "d"] = Nothing
-- | ```
find :: forall a. (a -> Boolean) -> Array a -> Maybe a
find f xs = unsafePartial (unsafeIndex xs) <$> findIndex f xs

-- | Find the first element in a data structure which satisfies
-- | a predicate mapping.
findMap :: forall a b. (a -> Maybe b) -> Array a -> Maybe b
findMap = findMapImpl Nothing isJust

foreign import findMapImpl
:: forall a b
. (forall c. Maybe c)
-> (forall c. Maybe c -> Boolean)
-> (a -> Maybe b)
-> Array a
-> Maybe b

-- | Find the first index for which a predicate holds.
-- |
-- | ```purescript
Expand Down Expand Up @@ -698,6 +733,26 @@ modifyAtIndices :: forall t a. Foldable t => t Int -> (a -> a) -> Array a -> Arr
modifyAtIndices is f xs =
ST.run (STA.withArray (\res -> traverse_ (\i -> STA.modify i f res) is) xs)

-- | Fold a data structure from the left, keeping all intermediate results
-- | instead of only the final result. Note that the initial value does not
-- | appear in the result (unlike Haskell's `Prelude.scanl`).
-- |
-- | ```
-- | scanl (+) 0 [1,2,3] = [1,3,6]
-- | scanl (-) 10 [1,2,3] = [9,7,4]
-- | ```
foreign import scanl :: forall a b. (b -> a -> b) -> b -> Array a -> Array b

-- | Fold a data structure from the right, keeping all intermediate results
-- | instead of only the final result. Note that the initial value does not
-- | appear in the result (unlike Haskell's `Prelude.scanr`).
-- |
-- | ```
-- | scanr (+) 0 [1,2,3] = [6,5,3]
-- | scanr (flip (-)) 10 [1,2,3] = [4,5,7]
-- | ```
foreign import scanr :: forall a b. (a -> b -> b) -> b -> Array a -> Array b

--------------------------------------------------------------------------------
-- Sorting ---------------------------------------------------------------------
--------------------------------------------------------------------------------
Expand Down
34 changes: 34 additions & 0 deletions test/Test/Data/Array.purs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Data.Array as A
import Data.Array.NonEmpty as NEA
import Data.Const (Const(..))
import Data.Foldable (for_, foldMapDefaultR, class Foldable, all, traverse_)
import Data.Traversable (scanl, scanr)
import Data.Maybe (Maybe(..), isNothing, fromJust)
import Data.Tuple (Tuple(..))
import Data.Unfoldable (replicateA)
Expand Down Expand Up @@ -131,6 +132,14 @@ testArray = do
assert $ [1, 2, 3] !! 6 == Nothing
assert $ [1, 2, 3] !! (-1) == Nothing

log "elem should return true if the array contains the given element at least once"
assert $ (A.elem 1 [1, 2, 1]) == true
assert $ (A.elem 4 [1, 2, 1]) == false

log "notElem should return true if the array does not contain the given element"
assert $ (A.notElem 1 [1, 2, 1]) == false
assert $ (A.notElem 4 [1, 2, 1]) == true

log "elemIndex should return the index of an item that a predicate returns true for in an array"
assert $ (A.elemIndex 1 [1, 2, 1]) == Just 0
assert $ (A.elemIndex 4 [1, 2, 1]) == Nothing
Expand All @@ -139,6 +148,15 @@ testArray = do
assert $ (A.elemLastIndex 1 [1, 2, 1]) == Just 2
assert $ (A.elemLastIndex 4 [1, 2, 1]) == Nothing

log "find should return the first element for which a predicate returns true in an array"
assert $ (A.find (_ /= 1) [1, 2, 1]) == Just 2
assert $ (A.find (_ == 3) [1, 2, 1]) == Nothing

log "findMap should return the mapping of the first element that satisfies the given predicate"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to also add a test case for an array where there is more than one element which satisfies the predicate here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in latest commits

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I should have been clearer: I think we should have a test which rules out the possibility that our findMap actually returns the last match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've updated the tests to account for that.

assert $ (A.findMap (\x -> if x > 3 then Just x else Nothing) [1, 2, 4]) == Just 4
assert $ (A.findMap (\x -> if x > 3 then Just x else Nothing) [1, 2, 1]) == Nothing
assert $ (A.findMap (\x -> if x > 3 then Just x else Nothing) [4, 1, 5]) == Just 4

log "findIndex should return the index of an item that a predicate returns true for in an array"
assert $ (A.findIndex (_ /= 1) [1, 2, 1]) == Just 1
assert $ (A.findIndex (_ == 3) [1, 2, 1]) == Nothing
Expand Down Expand Up @@ -237,6 +255,22 @@ testArray = do
assert $ A.modifyAtIndices [0, 2, 8] not [true, true, true, true] ==
[false, true, false, true]

log "scanl should return an array that stores the accumulated value at each step"
assert $ A.scanl (+) 0 [1,2,3] == [1, 3, 6]
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we test that these agree with the Foldable versions please? It's probably not necessary to compare with the Foldable versions for the other functions, but this one differs from Haskell and so there's more than one "sensible" option for how to implement it, so I think it's worth being a little bit more careful/deliberate in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you clarify what you mean by this? The tests I used were taken from the scanl docs and scanr docs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rereading this again, I think you mean this:

assert $ A.scanl (+)  0 [1,2,3] == scanl (+) 0 [1,2,3]

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, exactly.

assert $ A.scanl (-) 10 [1,2,3] == [9, 7, 4]

log "scanl should return the same results as its Foldable counterpart"
assert $ A.scanl (+) 0 [1,2,3] == scanl (+) 0 [1,2,3]
assert $ A.scanl (-) 10 [1,2,3] == scanl (-) 10 [1,2,3]

log "scanr should return an array that stores the accumulated value at each step"
assert $ A.scanr (+) 0 [1,2,3] == [6,5,3]
assert $ A.scanr (flip (-)) 10 [1,2,3] == [4,5,7]

log "scanr should return the same results as its Foldable counterpart"
assert $ A.scanr (+) 0 [1,2,3] == scanr (+) 0 [1,2,3]
assert $ A.scanr (flip (-)) 10 [1,2,3] == scanr (flip (-)) 10 [1,2,3]

log "sort should reorder a list into ascending order based on the result of compare"
assert $ A.sort [1, 3, 2, 5, 6, 4] == [1, 2, 3, 4, 5, 6]

Expand Down