5

Given a condition, I want to search through a list of elements and return the first element that reaches the condition, and the previous one.

In C/C++ this is easy :

int i = 0;
for(;;i++) if (arr[i] == 0) break;

After we get the index where the condition is met, getting the previous element is easy, through "arr[i-1]"

In Haskell:

  • dropWhile (/=0) list gives us the last element I want

  • takeWhile (/=0) list gives us the first element I want

But I don't see a way of getting both in a simple manner. I could enumerate the list and use indexing, but that seems messy. Is there a proper way of doing this, or a way of working around this?

duplode
  • 33,731
  • 7
  • 79
  • 150
  • 1
    you could use [break](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#v:break) - although as always, you'll need to be careful about what might happen if no elements of the list satisfy your condition – Robin Zigmond May 07 '19 at 10:31
  • Ah, I didn't think about that. Just saw that that also could end up as an issue in my C/C++ code. Break looks interesting, but I would still have to get "last" from the first list and "head" from the second. – meatandmahjong May 07 '19 at 10:46
  • 1
    yes, which is why as I said you need to be careful of the edge cases. But that's no different from using `dropWhile` and/or `takeWhile`. (And it's an inherent problem with getting a value from a list - the list could be empty for example. Presumably you have some way of handling this in your C/C++ code, and you need a way to handle in in Haskell too, likely by using `Maybe`.) – Robin Zigmond May 07 '19 at 10:52
  • 1
    You could probably use something similar to [`breakOn` from *extra*](https://hackage.haskell.org/package/extra/docs/Data-List-Extra.html#v:breakOn). – duplode May 07 '19 at 11:00
  • 2
    What should happen if the first element matches your predicate? (I note that your C code would simply be a memory safety violation in this case.) – Daniel Wagner May 07 '19 at 14:03

5 Answers5

8

I would zip the list with its tail so that you have pairs of elements available. Then you can just use find on the list of pairs:

f :: [Int] -> Maybe (Int, Int)
f xs = find ((>3) . snd) (zip xs (tail xs))

> f [1..10]
Just (3,4)

If the first element matches the predicate this will return Nothing (or the second match if there is one) so you might need to special-case that if you want something different.

As Robin Zigmond says break can also work:

g :: [Int] -> (Int, Int)
g xs = case break (>3) xs of (_, []) -> error "not found"
                             ([], _) -> error "first element"
                             (ys, z:_) -> (last ys, z)

(Or have this return a Maybe as well, depending on what you need.)

But this will, I think, keep the whole prefix ys in memory until it finds the match, whereas f can start garbage-collecting the elements it has moved past. For small lists it doesn't matter.

David Fletcher
  • 2,590
  • 1
  • 12
  • 14
  • Just seeing `zip` made me gloss over your solution due to my assumption that having to go through elements and zipping them together would be slow, but that's probably wrong due to laziness? – meatandmahjong May 07 '19 at 11:59
  • 1
    I'm not sure how clever the optimizer can be here. I would guess it won't avoid allocating the pairs but I could be wrong. Laziness at least means it won't do this past the point where it finds an element. A solution with break allocates a similar amount. Possibly for the best performance you need to write a manual recursion, but I don't know without benchmarking. – David Fletcher May 07 '19 at 13:10
  • 2
    `zip` only produces the pairs it needs to: `zip (a:as) (b:bs) = (a,b) : zip as bs`. – chepner May 07 '19 at 14:30
4

I would use a zipper-like search:

type ZipperList a = ([a], [a])

toZipperList :: [a] -> ZipperList a
toZipperList = (,) []

moveUntil' :: (a -> Bool) -> ZipperList a -> ZipperList a
moveUntil' _ (xs, []) = (xs, [])
moveUntil' f (xs, (y:ys))
    | f y       = (xs, (y:ys))
    | otherwise = moveUntil' f (y:xs, ys)

moveUntil :: (a -> Bool) -> [a] -> ZipperList a
moveUntil f = moveUntil' f . toZipperList

example :: [Int]
example = [2,3,5,7,11,13,17,19]

result :: ZipperList Int
result = moveUntil (>10) example -- ([7,5,3,2], [11,13,17,19])

The good thing about zippers is that they are efficient, you can access as many elements near the index you want, and you can move the focus of the zipper forwards and backwards. Learn more about zippers here:

http://learnyouahaskell.com/zippers

Note that my moveUntil function is like break from the Prelude but the initial part of the list is reversed. Hence you can simply get the head of both lists.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
3

A non-awkward way of implementing this as a fold is making it a paramorphism. For general explanatory notes, see this answer by dfeuer (I took foldrWithTails from it):

-- The extra [a] argument f takes with respect to foldr
-- is the tail of the list at each step of the fold.  
foldrWithTails :: (a -> [a] -> b -> b) -> b -> [a] -> b
foldrWithTails f n = go
    where
    go (a : as) = f a as (go as)
    go [] = n

boundary :: (a -> Bool) -> [a] -> Maybe (a, a)
boundary p = foldrWithTails findBoundary Nothing
    where
    findBoundary x (y : _) bnd
        | p y = Just (x, y)
        | otherwise = bnd
    findBoundary _ [] _ = Nothing

Notes:

  • If p y is true we don't have to look at bnd to get the result. That makes the solution adequately lazy. You can check that by trying out boundary (> 1000000) [0..] in GHCi.

  • This solution gives no special treatment to the edge case of the first element of the list matching the condition. For instance:

    GHCi> boundary (<1) [0..9]
    Nothing
    GHCi> boundary even [0..9]
    Just (1,2)
    
duplode
  • 33,731
  • 7
  • 79
  • 150
  • Surely you didn't mean `Nothing <|> bnd`. That's just `bnd`! Perhaps you intended `findBoundary x (y : _) bnd = ((x,y) <$ guard (p y)) <|> bnd` or similar? – dfeuer May 08 '19 at 05:21
  • @dfeuer Oops! That was a thinko: it is indeed just `bnd`, though I rather like your spelling using `guard` -- or perhaps [`ensure`](https://stackoverflow.com/a/49282010/2751851)? – duplode May 08 '19 at 09:50
  • `ensure` doesn't look quite strong enough for the job. You'd need to `fmap` to complete the pair, and that's just getting silly. – dfeuer May 08 '19 at 12:30
2

There's several alternatives; either way, you'll have to implement this yourself. You could use explicit recursion:

getLastAndFirst :: (a -> Bool) -> [a] -> Maybe (a, a)
getLastAndFirst p (x : xs@(y:ys))
    | p y = Just (x, y)
    | otherwise = getLastAndFirst p xs
getLastAndFirst _ [] = Nothing

Alternately, you could use a fold, but that would look fairly similar to the above, except less readable.

A third option is to use break, as suggested in the comments:

getLastAndFirst' :: (a -> Bool) -> [a] -> Maybe (a,a)
getLastAndFirst' p l =
    case break p l of
        (xs@(_:_), (y:_)) -> Just (last xs, y)
        _ -> Nothing
bradrn
  • 8,337
  • 2
  • 22
  • 51
  • What should happen if the first element satisfies the predicate? (Depending on the answer this might complicate a fold solution further.) – Micha Wiedenmann May 07 '19 at 14:35
  • @MichaWiedenmann In this case it would just return `Nothing`. That would be a problem in this case though. – bradrn May 07 '19 at 21:41
1
(\(xs, ys) -> [last xs, head ys]) $ break (==0) list

Using break as Robin Zigmond suggested ended up short and simple, not using Maybe to catch edge-cases, but I could replace the lambda with a simple function that used Maybe.


I toyed a bit more with the solution and came up with

breakAround :: Int -> Int -> (a -> Bool) -> [a] -> [a]
breakAround m n cond list = (\(xs, ys) -> (reverse (reverse take m (reverse xs))) ++ take n ys) $ break (cond) list

which takes two integers, a predicate, and a list of a, and returns a single list of m elements before the predicate and n elements after.

Example: breakAround 3 2 (==0) [3,2,1,0,10,20,30] would return [3,2,1,0,10]

duplode
  • 33,731
  • 7
  • 79
  • 150
  • 1
    I find it odd that your result is a two element list. Wouldn't a tuple (i.e. `(last xs, head ys)`) fit better? – Micha Wiedenmann May 07 '19 at 14:33
  • I wanted it as a list so I could extend it like shown in my updated answer, where I can supply the function with how many elements I want from before and after the predicate I want, all in a single list. – meatandmahjong May 07 '19 at 16:48