packages feed

disco-0.2: src/Disco/Util.hs

{-# LANGUAGE ImportQualifiedPost #-}

-- |
-- Module      :  Disco.Util
-- Copyright   :  disco team and contributors
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Miscellaneous utilities.
module Disco.Util where

import Control.Applicative (Alternative)
import Control.Monad (guard)
import Data.Bifunctor (bimap)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M

infixr 1 ==>

-- | A synonym for pairing which makes convenient syntax for
--   constructing literal maps via M.fromList.
(==>) :: a -> b -> (a, b)
(==>) = (,)

-- | Flipped variant of 'map'.
for :: [a] -> (a -> b) -> [b]
for = flip map

-- | A variant of 'Map' indexing that throws a custom error message
--   in case the key is not found, to help with debugging.
(!) :: (Show k, Ord k) => M.Map k v -> k -> v
m ! k = case M.lookup k m of
  Nothing -> error $ "key " ++ show k ++ " is not an element in the map"
  Just v -> v

-- | Find the maximum of a list of positive numbers, yielding 0 in the
--   case of an empty list.
maximum0 :: (Num a, Ord a) => [a] -> a
maximum0 [] = 0
maximum0 xs = maximum xs

-- | A variant of 'filter' that returns a @Maybe (NonEmpty a)@ instead
--   of a regular list.
filterNE :: (a -> Bool) -> NonEmpty a -> Maybe (NonEmpty a)
filterNE p = NE.nonEmpty . NE.filter p

-- | A variant of 'partition' that returns @Maybe (NonEmpty a)@s instead
--   of regular lists.
partitionNE :: (a -> Bool) -> NonEmpty a -> (Maybe (NonEmpty a), Maybe (NonEmpty a))
partitionNE p as = (filterNE p as, filterNE (not . p) as)

-- | A variant of 'partitionEithers' for nonempty lists.  If the
--   result is Left, it means all the inputs were Left.  If the result
--   is Right, we definitely have some Rights, and possibly some Lefts
--   as well.  This properly encodes the fact that at least one result
--   list must be nonempty.
partitionEithersNE :: NonEmpty (Either a b) -> Either (NonEmpty a) ([a], NonEmpty b)
partitionEithersNE = foldr1 combine . NE.map (bimap NE.singleton (([],) . NE.singleton))
 where
  combine :: Either (NonEmpty a) ([a], NonEmpty b) -> Either (NonEmpty a) ([a], NonEmpty b) -> Either (NonEmpty a) ([a], NonEmpty b)
  combine (Left as1) (Left as2) = Left (NE.append as1 as2)
  combine (Left as1) (Right (as2, bs)) = Right (NE.toList as1 ++ as2, bs)
  combine (Right (as1, bs)) (Left as2) = Right (as1 ++ NE.toList as2, bs)
  combine (Right (as1, bs1)) (Right (as2, bs2)) = Right (as1 ++ as2, NE.append bs1 bs2)

-- | Iterate a function until finding the first value that satisfies
--   the given predicate.  @iterUntil f p@ is equivalent to @head
--   . filter p . iterate f@ but does not trigger a partiality
--   warning.
iterUntil :: (a -> a) -> (a -> Maybe b) -> a -> b
iterUntil f p a = case p a of
  Just b -> b
  _ -> iterUntil f p (f a)

-- | Allow a value through only if it satisfies the given predicate.
gate :: Alternative f => (a -> Bool) -> a -> f a
gate p a = a <$ guard (p a)