As pointed out by owl57, the Haskell translation is incorrect and the correct implementation isn't quite so trivial. I don't see a way to pull the :count argument out into a separate function. We certainly can for skip, though, and I might.
Prelude> skipThen 1 (removeIfNot even 3) [2,3,4,5,6,7,8,9]
[2,4,6,8,9]
as implemented:
removeIfNot p count = go count
where
go _ [] = []
go 0 list = list
go n (x:xs) | p x = x:go n xs
| otherwise = go (n - 1) xs
skipThen count f list =
let (pre,suf) = split count list
in pre ++ f suf
You could decompose removeIfNot a bit further by representing the intermediate stages as pairs of lists (result + unprocessed input):
import Data.Bifunctor (bimap, first)
type Split a = ([a], [a])
unsplit :: Split a -> [a]
unsplit = uncurry (++)
splitStart :: [a] -> Split a
splitStart xs = ([], xs)
skip :: Int -> Split a -> Split a
skip n (xs, ys) = first (xs ++) (splitAt n ys)
iterateN :: Int -> (a -> a) -> a -> a
iterateN n f = (!! n) . iterate f
removeOneIfNot :: (a -> Bool) -> Split a -> Split a
removeOneIfNot p (xs, ys) = bimap (xs ++) (drop 1) (span p ys)
GHCI> unsplit . iterateN 3 (removeOneIfNot even) . skip 5 . splitStart $ [1..15]
[1,2,3,4,5,6,8,10,12,13,14,15]
With one more combinator we can decompose this slightly further:
stepSplit :: ([a] -> Split a) -> Split a -> Split a
stepSplit f = uncurry first . bimap (++) f
skip n = stepSplit (splitAt n)
removeOneIfNot p = stepSplit (second (drop 1) . span p)
Or in perhaps-more-familiar monadic terms:
import Control.Monad (replicateM_)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.State (StateT, evalStateT, state, get)
import Control.Monad.Trans.Writer (WriterT, execWriterT, tell)
import Data.Bifunctor (second)
import Data.Functor.Identity (Identity, runIdentity)
type SplitterT b m = WriterT [b] (StateT [b] m)
type Splitter b = SplitterT b Identity
splitter :: Monad m => ([b] -> ([b], [b])) -> SplitterT b m ()
splitter f = lift (state f) >>= tell
execSplitterT :: Monad m => SplitterT b m a -> [b] -> m [b]
execSplitterT m = evalStateT (execWriterT (m >> lift get >>= tell))
execSplitter :: Splitter b a -> [b] -> [b]
execSplitter m = runIdentity . execSplitterT m
skip :: Monad m => Int -> SplitterT b m ()
skip n = splitter (splitAt n)
removeOneIfNot :: Monad m => (b -> Bool) -> SplitterT b m ()
removeOneIfNot p = splitter (second (drop 1) . span p)
GHCI> execSplitter (skip 5 >> replicateM_ 3 (removeOneIfNot even)) [1..15]
[1,2,3,4,5,6,8,10,12,13,14,15]
It's hard to say which version is clearer. It probably depends on what you're used to. The code is essentially the same under the Writer/State abstraction, with `execSplitter` replacing both `unsplit` and `splitStart` and monadic bind in place of function composition. The `Splitter b ()` type is isomorphic to the `[b] -> ([b], [b])` used for the argument to `stepSplit` in the first version.
"Let R be a sequence of integers, indexed from 0. Determine the indices where R is odd, as a sequence of indices in ascending order. Remove the zero index, if it is present. Then take the first three of the remaining indices. Remove the elements with those indices from R."
This seems verbose, but watch it do something Common Lisp's remove-if will choke on:
range generates a lazy list. where generates the indices lazily. drop-while and take are also lazy, and I just coded reject so that, after the indices have been exhausted, if the remainder of the list is lazy, then it just tacks that lazy suffix into the output. I.e. it's not fully lazy, but in this case it works because the list of indices was trimmed to a finite length.
I think in release 242, I will might include reject, as a fully lazy version supporting a lazy, infinite list of indices as well as sequence; and will fix select to also be fully lazy.
Thank you, I've mailed author, he updated the post
> UPDATE 2020-08-03: I no longer stand by the content in this post. I think the overall sentiment is marginally accurate; however, the details in the post are incorrect (as many have pointed out over the years).
> As has been pointed out, remove-if-not’s start/count parameters behave differently and cannot easily be separated out of the function, a design trade-off that I appreciate.
The start argument specified where to start removing things and the end or count arguments specify where to stop removing things (ie at an index or after a certain number of elements respectively).
I claim this is a pretty good argument against the keyword arguments as they don’t necessarily do what one expects.
Honestly, I think the mistake comes from thinking of `remove-if(-not)' as `filter'.
When I see code saying (remove-if #'condition '(1 2 3 4 5 6) :start 5 :count 3), I would be pretty surprised if it removed anything prior to the 5th element. :count is worse, just by looking at this code I would not be sure whether it would always remove 3 elements (at most) or if it would remove all elements within a subsequence of 3 elements starting at index 5.