One-lining NonEmpty permutations

I’ve recently been thinking about isomorphic presentations of datatypes, and how best to approach leveraging the different representations simultaneously. I wrote a small library llun intended as a drop-in replacement for Data.List.NonEmpty, and was able to use pattern synonyms to better understand the problem I was trying to attack.

Non-empty by construction

Non-empty lists are one of the most fundamental examples of the applications of type safety. A regular list has two modes of construction:

data List x where
  Nil :: List x
  (:) :: x -> List x -> List x

-- [1, 2, 3] is semantically equal to (1 : (2 : (3 : Nil)))

However, a NonEmpty list only has one constructor, so does not permit the empty case:

data NonEmpty x where
  (:|) :: x -> List x -> NonEmpty x

This means that whenever we have a value of type NonEmpty, we have a projection function head :: (\(x :| _) -> x) :: NonEmpty x -> x. The same cannot be said for List x since we cannot know in general how to deconstruct Nil; instead we resort to listToMaybe :: List x -> Maybe x to handle this empty case.

The definition of NonEmpty above references List, but it doesn’t have to. There is a mathematically isomorphic definition with two constructors:

data NonEmpty' x where
  End :: x -> NonEmpty' x
  And :: x -> NonEmpty' x -> NonEmpty' x

-- (End x) ~ (x : [])
-- (And x xs) ~ (x : xs)

It’s like if we just stuck an x onto the constructors for List! And in fact there’s another isomorphic definition

data NonEmpty'' x where
  N :: x -> Maybe (NonEmpty'' x) -> NonEmpty'' x

-- (x `N` Nothing) ~ (End x)
-- (x `N` Just xs) ~ (And x xs)

Just because they’re mathematically isomorphic doesn’t mean they’re semantically the same; different perspectives can yield useful insights about a problem. To avoid a bunch of different data types that can’t easily be converted, we’re going to use the following pattern synonyms in the rest of this post:

infixr 5 :|, :&, :&?



data NonEmpty x = x :| [x]

nonEmpty :: [x] -> Maybe (NonEmpty x)
nonEmpty = \case
  [] -> Nothing
  x : xz -> Just (x :| xz)



pattern End :: x -> NonEmpty x
pattern End x = x :| []

pattern (:&) :: x -> NonEmpty x -> NonEmpty x
pattern x :& xs <- (x :| ~(nonEmpty -> Just xs))
  where
    x :& ~(y :| xz) = x :| (y : xz)

{-# COMPLETE End, (:&) #-}



pattern (:&?) :: x -> Maybe (NonEmpty x) -> NonEmpty x
pattern x :&? mxs <- (x :| ~(nonEmpty -> mxs))
  where
    x :&? mxs = maybe (End x) (x :&) mxs

{-# COMPLETE (:&?) #-}

The {-# COMPLETE #-} pragmas in the above snippet assert to GHC that the listed patterns cover all the cases, and that it doesn’t need to emit warnings for non-exhaustive pattern matches if all of them show up. This is essentially the same as providing a different “constructor set” for the data type, without the added conversion cost. They can even be used in expressions because they are bidirectional: implicitly in End using = but made explicit in both (:&) and (:&?) with <- and where. For more information on pattern synonyms, see the GHC user’s guide.

We can also write some helper functions in the style of these patterns to make our life easier while keeping our eDSL consistent:

(&:) :: NonEmpty x -> x -> NonEmpty x
(x :| xs) &: y = x :| (xs <> [y])

(<&) :: NonEmpty x -> [x] -> NonEmpty x
(x :| xs) <& ys = x :| (xs <> ys)

(&>) :: [x] -> NonEmpty x -> NonEmpty x
xs &> ys = case xs of
  [] -> ys
  (x : xs) -> x : (xs &> ys)

To recap, we have three sets of complete pattern matches on NonEmpty x:

match1 = \case
  (x :| xz) -> _ -- x :: x, xz :: [x]

match2 = \case
  End x -> _ -- x :: x
  (x :& xs) -> _ -- x :: x, xs :: NonEmpty x

match3 = \case
  (x :&? mxs) -> _ -- x :: x, mxs :: Maybe (NonEmpty x)

List permutations

Let’s take a trip back to empty-list-land for this section, and savour the syntactic sugar. The permutations of a list are all the possible orderings of that list.

> sort (permutations [1..4])
[ [1,2,3,4]
, [1,2,4,3]
, [1,3,2,4]
, [1,3,4,2]
, [1,4,2,3]
, [1,4,3,2]
, [2,1,3,4]
, [2,1,4,3]
, [2,3,1,4]
, [2,3,4,1]
, [2,4,1,3]
, [2,4,3,1]
, [3,1,2,4]
, [3,1,4,2]
, [3,2,1,4]
, [3,2,4,1]
, [3,4,1,2]
, [3,4,2,1]
, [4,1,2,3]
, [4,1,3,2]
, [4,2,1,3]
, [4,2,3,1]
, [4,3,1,2]
, [4,3,2,1]
]

The current implementation in Data.List is the maximally lazy one. That means that it produces every permutation it can before moving on to the next element in the source list.

permutations [1 .. n] ==
  map (take n) $ take (factorial n) (permutations [1 ..])

The StackOverflow link above explains some of the logic behind the existing implementation, shown below for y’all who didn’t click the link. We could hijack this implementation with fromList . map fromList since we would be certain that the empty case never happens, but where’s the fun in that?

permutations :: [a] -> [[a]]
permutations xs0 = xs0 : perms xs0 []
  where
    perms :: forall a. [a] -> [a] -> [[a]]
    perms []     _  = []
    perms (t:ts) is = foldr interleave (perms ts (t:is)) (permutations is)
      where
        interleave :: [a] -> [[a]] -> [[a]]
        interleave xs r = let (_,zs) = interleave' id xs r in zs

        interleave' :: ([a] -> b) -> [a] -> [b] -> ([a], [b])
        interleave' _ []     r = (ts, r)
        interleave' f (y:ys) r = let (us,zs) = interleave' (f . (y:)) ys r
                                 in  (y:us, f (t:y:us) : zs)

Yeah. That’s no fun. Let’s figure out how to write the nonempty version from scratch.

We’re going to need some way of keeping track of how many elements we’ve seen already, and how many are left. We need to be able to see ahead and behind at the same time. So let’s work diagonally. (1)

diagonally :: NonEmpty x -> (NonEmpty x -> NonEmpty x -> y) -> Maybe (NonEmpty y)
diagonally xs withFutureAndPast =
  let futures' = tails xs
      pasts'   = inits xs
      -- Note that these are different from the empty versions:
      -- they do not include the empty tail or init. To make sure
      -- that we're taking these into account, we'll just
      -- call everything `Maybe` and tack `Nothing`s on either end.
      futures = (Just <$> futures') &: Nothing
      pasts   = Nothing :& (Just <$> pasts')
   in catMaybes $ -- Newbie level up: why not `sequence`?
        zipWith
          (liftM2 @Maybe withFutureAndPast)
          futures
          pasts

Okay, but what does this look like?

> let diagonals = flip diagonally (,)
> toList . fmap (toList *** toList) $ diagonals (1 :| [2, 3, 4])
[([2,3,4], [1]), ([3,4], [1,2]), ([4], [1,2,3])]

Each step diagonally gives us access to both sides of the list. Why does this help us? Well, because now at each step we can ensure that the past has been fully permuted before we look at the future. Now all we have to do is figure out how to do the recursive step.

permutations :: NonEmpty x -> NonEmpty (NonEmpty x)
permutations xs =
  let subperms = diagonally xs \(next :| futures) pasts -> do
        pastPerms <- permutations pasts
        -- Now that we have the past permutations, we need to stick `next`
        -- in there, in every place it can go, to find out all the permutations
        -- that we'll have made at our next diagonal.
   in undefined

Crap. Okay. Let’s figure out how to insert a value in front of every existing element of a list.

insertions :: x -> NonEmpty x -> NonEmpty (NonEmpty x)
insertions x l@(y :&? ys) =
  (x :& l) -- stick it on the front
    :&? -- if there's more,
    (fmap (y :&) . insertions x <$> ys) -- recurse one element inwards
> fmap toList $ insertions 'A' ('x' :| "xxxx")
"Axxxxx" :| ["xAxxxx","xxAxxx","xxxAxx","xxxxAx"]

Now we can try again:

permutations :: NonEmpty x -> NonEmpty (NonEmpty x)
permutations xs =
  -- subperms :: Maybe (NonEmpty (NonEmpty (NonEmpty x)))
  let subperms = diagonally xs \(next :| futures) pasts -> do
        -- pastPerms :: NonEmpty (NonEmpty x)
        pastPerms <- permutations pasts
        -- presentPerms :: NonEmpty (NonEmpty (NonEmpty x))
        let presentPerms =
              insertions next pastPerms
                -- tack on the unpermuted future bits to each present perm
                <&> (<& futures)
        pure presentPerms
   in xs :&? fmap @Maybe (join @NonEmpty) subperms

We finally join to squash all of the presentPerms into a single NonEmpty, because those are all of the permutations for a single step. We just had to insert next into every spot to generate them all. And now, in one line:

permutations :: NonEmpty x -> NonEmpty (NonEmpty x)
permutations xs =
  (xs :&?) . fmap join $ diagonally xs \(t :| ts) -> fmap (<& ts) . insertions t <=< permutations

But is this maximally lazy? Let’s test it.

take :: Int -> NonEmpty x -> Maybe (NonEmpty x)
take n (x :| xs)
  | n <= 0 = Nothing
  | otherwise = Just (x :| Data.List.take (n - 1) xs)

Permitting ourselves to convert to plain lists for demonstration purposes, we do see that this is the case:

> let three = 1 :| [2, 3]
> let foops = three &: undefined
>
> (toList . fmap toList) (permutations three)
[[1,2,3],[2,1,3],[3,1,2],[1,3,2],[3,2,1],[2,3,1]]
> fmap (maybe [] toList . take 3) . maybe [] toList $ take 6 (permutations foops)
[[1,2,3],[2,1,3],[3,1,2],[1,3,2],[3,2,1],[2,3,1]]

And the general property can also be witnessed:

permsLazy :: Int -> Bool
permsLazy n =
  let list = 0 :| [1 .. abs n - 1]
      nn = List.product [1 .. n]
   in (toList . fmap toList) (permutations list)
        == ( fmap (maybe [] toList . take n) . maybe [] toList $
              take nn (permutations (0 :| [1 ..]))
           )
> map permsLazy [1..]
[True,True,True,True,True,^CInterrupted.

(1) This is actually where I’ve swept some stuff under the rug: this whole thing only works because Data.List.inits is implemented tail-recursively with snoc-lists. Otherwise we’d be forcing the list and we’d hit _|_ on infinite inputs. I just used the fromList . map fromList trick for this nonempty inits. I guess I’m not a true magician!

4 Likes