This answer is inspired by a comment from luqui on a now-deleted question.
filterFirst
can be implemented in a fairly direct way in terms of span
:
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst p = (\(yeas, rest) -> yeas ++ drop 1 rest) . span p
span :: (a -> Bool) -> [a] -> ([a], [a])
splits the list in two at the first element for which the condition doesn't hold. After span
, we drop the first element of the second part of the list (with drop 1
rather than tail
so that we don't have to add a special case for []
), and reassemble the list with (++)
.
As an aside, there is a near-pointfree spelling of this implementation which I find too pretty not to mention:
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst p = uncurry (++) . second (drop 1) . span p
While span
is a higher order function, it would be perfectly understandable if you found this implementation disappointing in the context of your question. After all, span
is not much more fundamental than filterFirst
itself. Shouldn't we try going a little deeper, to see if we can capture the spirit of this solution while expressing it as a fold, or as some other recursion scheme?
I believe functions like filterFirst
can be fine demonstrations of hylomorphisms. A hylomorphism is an unfold (see my other answer for more on that) that generates an intermediate data structure followed by a fold which turns this data structure into something else. Though it might look like that would require two passes to get a result (one through the input structure, and another through the intermediate one), if the hylomorphism implemented properly (as done in the hylo
function of recursion-schemes) it can be done in a single pass, with the fold consuming pieces of the intermediate structure as they are generated by the unfold (so that we don't have to actually build it all only to tear it down).
Before we start, here is the boilerplate needed to run what follows:
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TemplateHaskell #-}
import Data.Functor.Foldable
import Data.Functor.Foldable.TH
The strategy here is picking an intermediate data structure for the hylomorphism that expresses the essence of what we want to achieve. In this case, we will use this cute thing:
data BrokenList a = Broken [a] | Unbroken a (BrokenList a)
-- I won't actually use those instances here,
-- but they are nice to have if you want to play with the type.
deriving (Eq, Show, Functor, Foldable, Traversable)
makeBaseFunctor ''BrokenList
BrokenList
is very much like a list (Broken
and Unbroken
mirror []
and (:)
, while the makeBaseFunctor
incantation generates a BrokenListF
base functor analogous to ListF
, with BrokenF
and UnbrokenF
constructors), except that it has another list attached at its end (the Broken
constructor). It expresses, in a quite literal way, the idea of a list being divided in two parts.
With BrokenList
at hand, we can write the hylomorphism. coalgSpan
is the operation used for the unfold, and algWeld
, the one used for the fold.
filterFirst p = hylo algWeld coalgSpan
where
coalgSpan = \case
[] -> BrokenF []
x : xs
| p x -> UnbrokenF x xs
| otherwise -> BrokenF xs
algWeld = \case
UnbrokenF x yeas -> x : yeas
BrokenF rest -> rest
coalgSpan
breaks the list upon hitting a x
element such that p x
doesn't hold. Not adding that element to the second part of the list (BrokenF xs
rather than BrokenF (x : xs)
) takes care of the filtering. As for algWeld
, it is used to concatenate the two parts (it is very much like what we would use to implement (++)
using cata
).
(For a similar example of BrokenList
in action, see the breakOn
implementation in Note 5 of this older answer of mine. It suggests what it would take to implement span
using this strategy.)
There are at least two good things about this hylo
-based implementation. Firstly, it has good performance (casual testing suggests that, if compiled with optimisations, it is at least as good as, and possibly slightly faster than, the most efficient implementations in other answers here). Secondly, it reflects very closely your original, explicitly recursive implementation of filterFirst
(or, at any rate, more closely than the fold-only and unfold-only implementations).