Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you write tailMay in Haskell for Foldable, without using unsafe functions?

Tags:

haskell

The signature should be:

tailMay :: Foldable f => f a -> Maybe (f a)

I was given this definition of headMay, which makes sense, and is (to me) rather clever:

headMay :: Foldable f => f a -> Maybe a
headMay = foldr (const . Just) Nothing

Unfortunately, I haven't been able to think of anything similar for tailMay, and similar implementations seem to use magic or unsafe functions.

If this isn't possible, something that works for an (Applicative t, Foldable t) would also be fine for my purposes (which I gather would also imply Traversable t).

like image 249
bbarker Avatar asked Nov 21 '19 14:11

bbarker


1 Answers

Tail only makes sense for list like structures, so it would be sensible to return a list, rather then the same container type f. Implementation is trivial and can be done in many ways, here is one.

import Control.Monad (guard)
import Data.Foldable (toList)

tailMay :: Foldable f => f a -> Maybe [a]
tailMay xs = guard (not (null xs)) >> Just (drop 1 (toList xs))

But, since it works for all list like structures, we could be cool and define it for all things that have is IsList.

{-# LANGUAGE TypeFamilies #-}
import Control.Monad (guard)
import Data.Foldable (toList)
import GHC.Exts (IsList(Item, fromList))

tailMay :: (IsList l, Item l ~ a, Foldable f) => f a -> Maybe l
tailMay xs = guard (not (null xs)) >> Just (fromList (drop 1 (toList xs)))

Edit

A nice benefit of the above solution is that you can change types from the argument to the result. For example you can start with a Vector and result with a Set. But that also has a down side, you need to specify the resulting type yourself:

So running this will result in a compile error:

λ> tailMay [1 :: Int]
<interactive>:24:1-18: error:
    • Illegal equational constraint Item l ~ Int
      (Use GADTs or TypeFamilies to permit this)
    • When checking the inferred type
        it :: forall l. (IsList l, Item l ~ Int) => Maybe l
λ> tailMay [1 :: Int] :: Maybe [Int]
Just []

We can easily restrict resulting type, if above is really a problem or simply an undesired effect for us:

tailMay :: (IsList l, Item l ~ a, Foldable f, l ~ f a) => f a -> Maybe l

After that it works great:

λ> import qualified Data.Vector as V
λ> tailMay $ V.enumFromTo 'a' 'd'
Just "bcd"
λ> tailMay [1 :: Int]
Just []
λ> tailMay V.empty
Nothing
λ> tailMay []
Nothing

Warning - It all would be great, but unfortunately there is no guarantee that fromList will have a total implementation:

λ> import qualified Data.List.NonEmpty as NE
λ> tailMay (5 NE.:| [] :: NE.NonEmpty Int)
Just *** Exception: NonEmpty.fromList: empty list

There is no general way to protect yourself from it, except by creating your own IsList like class and providing your own instances for all such data structures.

like image 74
lehins Avatar answered Nov 12 '22 12:11

lehins