packages feed

cardano-coin-selection-1.0.0: src/test/Test/Vector/Shuffle.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}

module Test.Vector.Shuffle
    ( -- * Simple
      shuffle
    , shuffleNonEmpty

      -- * Advanced
    , mkSeed
    , shuffleWith
    , shuffleNonEmptyWith
    ) where

import Prelude

import Control.Monad
    ( forM_ )
import Control.Monad.Trans.Class
    ( lift )
import Control.Monad.Trans.State.Strict
    ( evalStateT, state )
import Crypto.Hash
    ( hash )
import Crypto.Hash.Algorithms
    ( MD5 )
import Data.List.NonEmpty
    ( NonEmpty (..) )
import Data.Maybe
    ( fromMaybe )
import Data.Text
    ( Text )
import Data.Vector.Mutable
    ( IOVector )
import Data.Word
    ( Word8 )
import System.Random
    ( RandomGen, StdGen, mkStdGen, newStdGen, randomR )

import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import qualified Data.List.NonEmpty as NE
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV

-- | Generate a random generator seed from a text string
mkSeed :: Text -> StdGen
mkSeed = mkStdGen . toInt . quickHash . T.encodeUtf16LE
  where
    quickHash = BA.convert . hash @_ @MD5
    toInt = snd . BS.foldl' exponentiation (0,0)
      where
        exponentiation :: (Int, Int) -> Word8 -> (Int, Int)
        exponentiation (e, n) i = (e+1, n + fromIntegral i*2^e)

-- | Shuffles a list of elements.
--
-- >>> shuffle (outputs coinSel)
-- [...]
shuffle :: [a] -> IO [a]
shuffle xs = newStdGen >>= flip shuffleWith xs

-- | Like 'shuffle', but from a given seed. 'shuffle' will use a randomly
-- generate seed using 'newStdGen' from @System.Random@.
--
-- __Properties:__
--
-- - @shuffleWith g es == shuffleWith g es@
-- - @∃Δ> 1. g ≠g', length es > Δ⇒ shuffleWith g es ≠shuffleWith g' es@
shuffleWith :: RandomGen g => g -> [a] -> IO [a]
shuffleWith seed = modifyInPlace $ \v -> flip evalStateT seed $ do
    let (lo, hi) = (0, MV.length v - 1)
    forM_ [lo .. hi] $ \i -> do
      j <- fromInteger <$> state (randomR (fromIntegral lo, fromIntegral hi))
      lift $ swapElems v i j
  where
    swapElems :: IOVector a -> Int -> Int -> IO ()
    swapElems v i j = do
        x <- MV.read v i
        y <- MV.read v j
        MV.write v i y
        MV.write v j x

    modifyInPlace :: forall a. (IOVector a -> IO ()) -> [a] -> IO [a]
    modifyInPlace f xs = do
        v' <- V.thaw $ V.fromList xs
        f v'
        V.toList <$> V.freeze v'

-- | Shuffles a /non-empty/ list of elements.
--
-- See 'shuffle'.
--
shuffleNonEmpty :: NonEmpty a -> IO (NonEmpty a)
shuffleNonEmpty xs = newStdGen >>= flip shuffleNonEmptyWith xs

-- | Shuffles a /non-empty/ list of elements with the given random generator.
--
-- See 'shuffleWith'.
--
shuffleNonEmptyWith :: RandomGen g => g -> NonEmpty a -> IO (NonEmpty a)
shuffleNonEmptyWith g =
    fmap (fromMaybe raiseError . NE.nonEmpty) . shuffleWith g . NE.toList
  where
    raiseError = error "shuffleNonEmptyWith encountered an empty list."