[Haskell-cafe] about Haskell code written to be "too smart"

Claus Reinke claus.reinke at talk21.com
Thu Mar 26 10:18:02 EDT 2009


Continuing our adventures into stylistic and semantic differences:-)

Comparing the 'State' and explicit recursion versions

    takeListSt = evalState . mapM (State . splitAt)

    -- ..with a derivation leading to..

    takeListSt []    s = []
    takeListSt (h:t) s = x : takeListSt t s'
      where (x,s') = splitAt h s

instead of

    takeList [] _         =  []
    takeList _ []         =  []
    takeList (n : ns) xs  =  head : takeList ns tail
        where (head, tail) = splitAt n xs

we can see some differences, leading to different functions:

    *Main> null $ takeListSt [1] undefined
    False
    *Main> null $ takeList [1] undefined
    *** Exception: Prelude.undefined
    *Main> takeList [0] []
    []
    *Main> takeListSt [0] []
    [[]]

and similarly for the 'scanl' version

    takeListSc ns xs = zipWith take ns $ init $ scanl (flip drop) xs ns

Depending on usage, these differences might not matter, but what if
we want these different styles to lead to the same function, with only
stylistic and no semantic differences, taking the explicit recursion as
our spec?

In the 'State' version, the issue is that 'mapM' does not terminate
early, while the specification requires an empty list whenever 'xs'
(the state) is empty. Following the derivation at

http://www.haskell.org/pipermail/haskell-cafe/2009-March/058603.html

the first step where we have a handle on that is after unfolding
'sequence':

    takeListSt = evalState . foldr k (return []) . map (State . splitAt)
      where k m m' = do x<-m; xs<-m'; return (x:xs)

If we change that to

    takeListSt' = evalState . foldr k (return []) . map (State . splitAt)
      where k m m'    = cutNull $ do x<-m; xs<-m'; return (x:xs)
            cutNull m = do s<-get; if null s then return [] else m

and continue with the modified derivation, we should end up with
the right spec (I haven't done this, so you should check!-). This
isn't all that elegant any more, but support for 'mapM' with early
exit isn't all that uncommon a need, either, so one might expect
a 'mapM' variant that takes a 'cut' parameter to make it into the
libraries.

For the 'scanl' version, we have a more direct handle on the issue:
we can simply drop the offending extras from the 'scanl' result,
replacing 'init' with 'takeWhile (not.null)':

    takeListSc' ns xs = zipWith take ns $ takeWhile (not.null) $ scanl (flip drop) xs ns

A somewhat abbreviated derivation at the end of this message
seems to confirm that this matches the spec (as usual with proofs,
writing them down doesn't mean that they are correct, but that
readers can check whether they are).

(btw, both 'takeListSt'' and 'takeListSc'' pass Thomas' 'testP', as does
his 'partitions', but 'partitions' is not the same function as 'takeList':
consider 'null $ takeList [1] undefined' and 'takeList [0] []' ;-)

Someone suggested using 'mapAccumL' instead of 'State', and
that does indeed work, only that everything is the wrong way round:

    takeListMAL = (snd.) . flip (mapAccumL (((snd&&&fst).).(flip splitAt)))

This is an example where all the "cleverness" is spent on the
irrelevant details, giving them way too much importance. So one
might prefer a version that more clearly says that this is mostly
'mapAccumL splitAt', with some administratory complications
that might be ignored on cursory inspection:

    takeListMAL' = mapAccumL' splitAt'
      where splitAt' l n       = swap $ splitAt n l
            mapAccumL' f l acc = snd $ mapAccumL f acc l
            swap (x,y)         = (y,x)

Of course, this suffers from the "does not terminate early" issue,
but as this thread encourages us to look at functions we might
not otherwise consider, I thought I'd follow the suggestion, and
perhaps someone might want to modify it with a 'mapAccumL'
with cutoff, and demonstrate whether it matches the spec;-)

Claus

-- view transformation: reducing the level of abstraction

takeList ns xs = zipWith take ns $ takeWhile (not.null) $ scanl (flip drop) xs ns

-- fetch definitions of 'zipWith', 'takeWhile', and 'scanl'

takeList ns xs = zipWith take ns $ takeWhile (not.null) $ scanl (flip drop) xs ns
  where scanl f q ls = q : case  ls of
                             [] -> []
                             x:xs -> scanl f (f q x) xs
        takeWhile _ []                 = []
        takeWhile p (x:xs) | p x       = x : takeWhile p xs
                           | otherwise = []
        zipWith f (a:as) (b:bs) = f a b : zipWith f as bs
        zipWith _ _      _      = []

-- specialize for 'take', 'not.null', and 'flip drop'

takeList ns xs = zipWith ns $ takeWhile $ scanl xs ns
  where scanl q ls = q : case  ls of
                             [] -> []
                             x:xs -> scanl (drop x q) xs
        takeWhile []                    = []
        takeWhile (x:xs) | not (null x) = x : takeWhile xs
                         | otherwise    = []
        zipWith (a:as) (b:bs) = take a b : zipWith as bs
        zipWith _      _      = []

-- fuse 'takeWhile' and 'scanl' into 'tws'

takeList ns xs = zipWith ns $ tws xs ns
  where tws q ls | not (null q) = q : case  ls of
                                       [] -> []
                                       x:xs -> tws (drop x q) xs
                 | otherwise    = []
        zipWith (a:as) (b:bs) = take a b : zipWith as bs
        zipWith _      _      = []

-- fuse 'zipWith' and 'tws' into 'ztws'

takeList ns xs = ztws ns xs ns
  where ztws (a:as) q ls | not (null q) = take a q : case  ls of
                                                       [] -> []
                                                       x:xs -> ztws as (drop x q) xs
                         | otherwise    = []
        ztws _      _ _                 = []

-- 'ls' is 'as'

takeList ns xs = ztws ns xs
  where ztws (a:as) q | not (null q) = take a q : ztws as (drop a q)
                      | otherwise    = []
        ztws _      _                = []

-- remove indirection

takeList (a:as) q | not (null q) = take a q : takeList as (drop a q)
                  | otherwise    = []
takeList _      _                = []

-- replace guard by clause

takeList (a:as) [] = []
takeList (a:as) q  = take a q : takeList as (drop a q)
takeList _      _  = []

-- '_' in last clause has to be '[]'

takeList (a:as) [] = []
takeList (a:as) q  = take a q : takeList as (drop a q)
takeList []     _  = []

-- switch non-overlapping clauses

takeList []     _  = []
takeList (a:as) [] = []
takeList (a:as) q  = take a q : takeList as (drop a q)

-- for second parameter '[]', both ':' and '[]' in first parameter result in '[]'

takeList []     _  = []
takeList _      [] = []
takeList (a:as) q  = take a q : takeList as (drop a q)

-- (take a q,drop a q) = splitAt a q

takeList []     _  = []
takeList _      [] = []
takeList (a:as) q  = t : takeList as d
  where (t,d) = splitAt a q




More information about the Haskell-Cafe mailing list