chimera 0.3.1.0 → 0.3.2.0
raw patch · 7 files changed
+277/−48 lines, 7 filesdep +tasty-benchdep −gaugedep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: tasty-bench
Dependencies removed: gauge
Dependency ranges changed: base
API changes (from Hackage documentation)
+ Data.Chimera: tabulateFix' :: Vector v a => ((Word -> a) -> Word -> a) -> Chimera v a
Files
- Data/Chimera.hs +90/−20
- Data/Chimera/WheelMapping.hs +15/−2
- README.md +79/−6
- bench/Bench.hs +34/−6
- changelog.md +38/−0
- chimera.cabal +12/−12
- test/Test.hs +9/−2
Data/Chimera.hs view
@@ -8,8 +8,6 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -31,6 +29,7 @@ -- * Construction , tabulate , tabulateFix+ , tabulateFix' , iterate , cycle @@ -60,11 +59,15 @@ import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U -#if DefineRepresentable-import Control.Monad.Reader+#ifdef MIN_VERSION_mtl+import Control.Monad.Reader (MonadReader, ask, local)+#endif+#ifdef MIN_VERSION_distributive import Data.Distributive+#ifdef MIN_VERSION_adjunctions import qualified Data.Functor.Rep as Rep #endif+#endif import Data.Chimera.Compat import Data.Chimera.FromIntegral@@ -145,22 +148,24 @@ mzip as bs = tabulate (\w -> (index as w, index bs w)) mzipWith f as bs = tabulate $ \w -> f (index as w) (index bs w) -#if DefineRepresentable-+#ifdef MIN_VERSION_mtl instance MonadReader Word (Chimera V.Vector) where- ask = Rep.askRep- local = Rep.localRep+ ask = tabulate id+ local = flip $ (tabulate .) . (.) . index+#endif +#ifdef MIN_VERSION_distributive instance Distributive (Chimera V.Vector) where- distribute = Rep.distributeRep- collect = Rep.collectRep+ distribute = tabulate . flip (fmap . flip index)+ collect f = tabulate . flip ((<$>) . (. f) . flip index) +#ifdef MIN_VERSION_adjunctions instance Rep.Representable (Chimera V.Vector) where type Rep (Chimera V.Vector) = Word tabulate = tabulate index = index- #endif+#endif bits :: Int bits = fbs (0 :: Word)@@ -212,9 +217,35 @@ -- 4862 -- >>> take 10 (toList ch) -- [1,1,2,5,14,42,132,429,1430,4862]+--+-- __Note__: Only recursive function calls with decreasing arguments are memoized.+-- If full memoization is desired, use 'tabulateFix'' instead. tabulateFix :: G.Vector v a => ((Word -> a) -> Word -> a) -> Chimera v a tabulateFix uf = runIdentity $ tabulateFixM ((pure .) . uf . (runIdentity .)) +-- | Fully memoizing version of 'tabulateFix'.+-- This function will tabulate every recursive call,+-- but might allocate a lot of memory in doing so.+-- For example, the following piece of code calculates the+-- highest number reached by the+-- <https://en.wikipedia.org/wiki/Collatz_conjecture#Statement_of_the_problem Collatz sequence>+-- of a given number, but also allocates tens of gigabytes of memory,+-- because the Collatz sequence will spike to very high numbers.+--+-- >>> collatzF :: (Word -> Word) -> (Word -> Word)+-- >>> collatzF _ 0 = 0+-- >>> collatzF f n = if n <= 2 then 4 else n `max` f (if even n then n `quot` 2 else 3 * n + 1)+-- >>>+-- >>> maximumBy (comparing $ index $ tabulateFix' collatzF) [0..1000000]+-- ...+--+-- Using 'memoizeFix' instead fixes the problem:+--+-- >>> maximumBy (comparing $ memoizeFix collatzF) [0..1000000]+-- 56991483520+tabulateFix' :: G.Vector v a => ((Word -> a) -> Word -> a) -> Chimera v a+tabulateFix' uf = runIdentity $ tabulateFixM' ((pure .) . uf . (runIdentity .))+ -- | Monadic version of 'tabulateFix'. -- There are no particular guarantees about the order of recursive calls: -- they may be executed more than once or executed in different order.@@ -224,13 +255,37 @@ (Monad m, G.Vector v a) => ((Word -> m a) -> Word -> m a) -> m (Chimera v a)-tabulateFixM f = result+tabulateFixM = tabulateFixM_ Downwards++{-# SPECIALIZE tabulateFixM :: G.Vector v a => ((Word -> Identity a) -> Word -> Identity a) -> Identity (Chimera v a) #-}++-- | Monadic version of 'tabulateFix''.+tabulateFixM'+ :: forall m v a.+ (Monad m, G.Vector v a)+ => ((Word -> m a) -> Word -> m a)+ -> m (Chimera v a)+tabulateFixM' = tabulateFixM_ Full++-- | Memoization strategy, only used by 'tabulateFixM_'.+data Strategy = Full | Downwards++-- | Internal implementation for 'tabulateFixM' and 'tabulateFixM''.+tabulateFixM_+ :: forall m v a.+ (Monad m, G.Vector v a)+ => Strategy+ -> ((Word -> m a) -> Word -> m a)+ -> m (Chimera v a)+tabulateFixM_ strat f = result where result :: m (Chimera v a) result = Chimera <$> V.generateM (bits + 1) tabulateSubVector tabulateSubVector :: Int -> m (v a)- tabulateSubVector 0 = G.singleton <$> fix f 0+ tabulateSubVector 0 = G.singleton <$> case strat of+ Downwards -> fix f 0+ Full -> f (\k -> flip index k <$> result) 0 tabulateSubVector i = subResult where subResult = G.generateM ii (\j -> f fixF (int2word (ii + j)))@@ -241,12 +296,12 @@ fixF k | k < int2word ii = flip index k <$> result- | k < int2word ii `shiftL` 1+ | k <= int2word ii `shiftL` 1 - 1 = (`V.unsafeIndex` (word2int k - ii)) <$> subResultBoxed | otherwise- = f fixF k--{-# SPECIALIZE tabulateFixM :: G.Vector v a => ((Word -> Identity a) -> Word -> Identity a) -> Identity (Chimera v a) #-}+ = case strat of+ Downwards -> f fixF k+ Full -> flip index k <$> result -- | 'iterate' @f@ @x@ returns an infinite stream -- of repeated applications of @f@ to @x@.@@ -281,7 +336,7 @@ index (Chimera vs) i = (vs `V.unsafeIndex` (fbs i - lz)) `G.unsafeIndex`- (word2int (i .&. complement ((1 `shiftL` (fbs i - 1)) `unsafeShiftR` lz)))+ word2int (i .&. complement ((1 `shiftL` (fbs i - 1)) `unsafeShiftR` lz)) where lz :: Int !lz = word2int (clz i)@@ -313,6 +368,8 @@ -- would compute @f@ @n@ only once -- and cache the result in 'VChimera'. -- This is just a shortcut for 'index' '.' 'tabulate'.+-- When @a@ is 'U.Unbox', it is faster to use+-- 'index' ('tabulate' @f@ :: 'UChimera' @a@). -- -- prop> memoize f n = f n memoize :: (Word -> a) -> (Word -> a)@@ -321,18 +378,20 @@ -- | For a given @f@ memoize a recursive function 'fix' @f@, -- caching results in 'VChimera'. -- This is just a shortcut for 'index' '.' 'tabulateFix'.+-- When @a@ is 'U.Unbox', it is faster to use+-- 'index' ('tabulateFix' @f@ :: 'UChimera' @a@). -- -- prop> memoizeFix f n = fix f n -- -- For example, imagine that we want to memoize -- <https://en.wikipedia.org/wiki/Fibonacci_number Fibonacci numbers>: ----- >>> fibo n = if n < 2 then fromIntegral n else fibo (n - 1) + fibo (n - 2)+-- >>> fibo n = if n < 2 then toInteger n else fibo (n - 1) + fibo (n - 2) -- -- Can we find @fiboF@ such that @fibo@ = 'fix' @fiboF@? -- Just replace all recursive calls to @fibo@ with @f@: ----- >>> fiboF f n = if n < 2 then fromIntegral n else f (n - 1) + f (n - 2)+-- >>> fiboF f n = if n < 2 then toInteger n else f (n - 1) + f (n - 2) -- -- Now we are ready to use 'memoizeFix': --@@ -340,6 +399,17 @@ -- 55 -- >>> memoizeFix fiboF 100 -- 354224848179261915075+--+-- This function can be used even when arguments+-- of recursive calls are not strictly decreasing,+-- but they might not get memoized. If this is not desired+-- use 'tabulateFix'' instead.+-- For example, here is a routine to measure the length of+-- <https://oeis.org/A006577 Collatz sequence>:+--+-- >>> collatzF f n = if n <= 1 then 0 else 1 + f (if even n then n `quot` 2 else 3 * n + 1)+-- >>> memoizeFix collatzF 27+-- 111 memoizeFix :: ((Word -> a) -> Word -> a) -> (Word -> a) memoizeFix = index @V.Vector . tabulateFix
Data/Chimera/WheelMapping.hs view
@@ -66,6 +66,7 @@ -- {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE UnboxedTuples #-} @@ -162,7 +163,7 @@ -- -- prop> toWheel210 . fromWheel210 == id toWheel210 :: Word -> Word-toWheel210 i@(W# i#) = q `shiftL` 5 + q `shiftL` 4 + W# (indexWord8OffAddr# table# (word2Int# r#))+toWheel210 i@(W# i#) = q `shiftL` 5 + q `shiftL` 4 + W# tableEl# where !(q, W# r#) = case bits of 64 -> (q64, r64)@@ -173,6 +174,12 @@ q64 = W# z1# `shiftR` 6 r64 = i - q64 * 210 + tableEl# =+#if MIN_VERSION_base(4,16,0)+ word8ToWord#+#endif+ (indexWord8OffAddr# table# (word2Int# r#))+ table# :: Addr# table# = "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\SOH\SOH\STX\STX\STX\STX\ETX\ETX\EOT\EOT\EOT\EOT\ENQ\ENQ\ENQ\ENQ\ENQ\ENQ\ACK\ACK\a\a\a\a\a\a\b\b\b\b\t\t\n\n\n\n\v\v\v\v\v\v\f\f\f\f\f\f\r\r\SO\SO\SO\SO\SO\SO\SI\SI\SI\SI\DLE\DLE\DC1\DC1\DC1\DC1\DC1\DC1\DC2\DC2\DC2\DC2\DC3\DC3\DC3\DC3\DC3\DC3\DC4\DC4\DC4\DC4\DC4\DC4\DC4\DC4\NAK\NAK\NAK\NAK\SYN\SYN\ETB\ETB\ETB\ETB\CAN\CAN\EM\EM\EM\EM\SUB\SUB\SUB\SUB\SUB\SUB\SUB\SUB\ESC\ESC\ESC\ESC\ESC\ESC\FS\FS\FS\FS\GS\GS\GS\GS\GS\GS\RS\RS\US\US\US\US !!\"\"\"\"\"\"######$$$$%%&&&&''''''(())))))****++,,,,--........../"# -- map Data.Char.chr [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 23, 23, 23, 23, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 40, 40, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 44, 44, 44, 44, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47]@@ -187,7 +194,7 @@ -- >>> map fromWheel210 [0..9] -- [1,11,13,17,19,23,29,31,37,41] fromWheel210 :: Word -> Word-fromWheel210 i@(W# i#) = q * 210 + W# (indexWord8OffAddr# table# (word2Int# r#))+fromWheel210 i@(W# i#) = q * 210 + W# tableEl# where !(q, W# r#) = case bits of 64 -> (q64, r64)@@ -197,6 +204,12 @@ !(# z1#, _ #) = timesWord2# m# i# q64 = W# z1# `shiftR` 5 r64 = i - q64 `shiftL` 5 - q64 `shiftL` 4++ tableEl# =+#if MIN_VERSION_base(4,16,0)+ word8ToWord#+#endif+ (indexWord8OffAddr# table# (word2Int# r#)) table# :: Addr# table# = "\SOH\v\r\DC1\DC3\ETB\GS\US%)+/5;=CGIOSYaegkmqy\DEL\131\137\139\143\149\151\157\163\167\169\173\179\181\187\191\193\197\199\209"#
README.md view
@@ -1,9 +1,9 @@-# chimera+# chimera [](https://hackage.haskell.org/package/chimera) [](http://stackage.org/lts/package/chimera) [](http://stackage.org/nightly/package/chimera) Lazy infinite compact streams with cache-friendly O(1) indexing and applications for memoization. -----+## Introduction Imagine having a function `f :: Word -> a`, which is expensive to evaluate. We would like to _memoize_ it,@@ -96,6 +96,75 @@ isPrime' = memoizeFix isPrimeF ``` +## Example 3++No manual on memoization is complete+without Fibonacci numbers:++```haskell+fibo :: Word -> Integer+fibo = memoizeFix $ \f n -> if n < 2 then toInteger n else f (n - 1) + f (n - 2)+```++No cleverness involved: just write a recursive function+and let `memoizeFix` take care about everything else:++```haskell+> fibo 100+354224848179261915075+```++## What about non-`Word` arguments?++`Chimera` itself can memoize only `Word -> a` functions, which sounds restrictive.+That is because we decided to outsource+enumerating of user's datatypes to other packages, e. g.,+[`cantor-pairing`](http://hackage.haskell.org/package/cantor-pairing).+Use `fromInteger . fromCantor` to convert data to `Word`+and `toCantor . toInteger` to go back.++Also, `Data.Chimera.ContinuousMapping` covers several simple cases,+such as `Int`, pairs and triples.++## Benchmarks++How important is to store cached data as a flat array instead of a lazy binary tree?+Let us measure the maximal length of [Collatz sequence](https://oeis.org/A006577),+using `chimera` and `memoize` packages.++```haskell+{-# LANGUAGE TypeApplications #-}+import Data.Chimera+import Data.Function.Memoize+import Data.Ord+import Data.List+import Data.Time.Clock++collatzF :: Integral a => (a -> a) -> (a -> a)+collatzF f n = if n <= 1 then 0 else 1 + f (if even n then n `quot` 2 else 3 * n + 1)++measure :: (Integral a, Show a) => String -> (((a -> a) -> (a -> a)) -> (a -> a)) -> IO ()+measure name memo = do+ t0 <- getCurrentTime+ print $ maximumBy (comparing (memo collatzF)) [0..1000000]+ t1 <- getCurrentTime+ putStrLn $ name ++ " " ++ show (diffUTCTime t1 t0)++main :: IO ()+main = do+ measure "chimera" Data.Chimera.memoizeFix+ measure "memoize" (Data.Function.Memoize.memoFix @Int)+```++Here `chimera` appears to be 20x faster than `memoize`:++```+837799+chimera 0.428015s+837799+memoize 8.955953s+```+ ## Magic and its exposure Internally `Chimera` is represented as a _boxed_ vector@@ -106,9 +175,9 @@ ``` Assuming 64-bit architecture, the outer vector consists of 65 inner vectors-of sizes 1, 1, 2, 2<sup>2</sup>, ..., 2<sup>63</sup>. Since the outer vector+of sizes 1, 1, 2, 2², ..., 2⁶³. Since the outer vector is boxed, inner vectors are allocated on-demand only: quite fortunately,-there is no need to allocate all 2<sup>64</sup> elements at once.+there is no need to allocate all 2⁶⁴ elements at once. To access an element by its index it is enough to find out to which inner vector it belongs, which, thanks to the doubling pattern of sizes,@@ -120,15 +189,19 @@ over a thin set of indices. One can argue that this structure is not infinite,-because it cannot handle more than 2<sup>64</sup> elements.+because it cannot handle more than 2⁶⁴ elements. I believe that it is _infinite enough_ and no one would be able to exhaust its finiteness any time soon. Strictly speaking, to cope with indices out of `Word` range and `memoize` [Ackermann function](https://en.wikipedia.org/wiki/Ackermann_function), one could use more layers of indirection, raising access time-to O([log<sup>*</sup>](https://en.wikipedia.org/wiki/Iterated_logarithm) n).+to O([log ⃰](https://en.wikipedia.org/wiki/Iterated_logarithm) n). I still think that it is morally correct to claim O(1) access, because all asymptotic estimates of data structures are usually made under an assumption that they contain less than `maxBound :: Word` elements (otherwise you can not even treat pointers as a fixed-size data).++## Additional resources++* [Lazy streams with O(1) access](https://github.com/Bodigrim/my-talks/raw/master/londonhaskell2020/slides.pdf), London Haskell, 25.02.2020.
bench/Bench.hs view
@@ -1,14 +1,26 @@+{-# LANGUAGE CPP #-}+ module Main where -import Control.Monad.State+import Control.Monad.State (evalState, put, get) import Data.Chimera-import Gauge.Main+import Test.Tasty.Bench import System.Random +#ifdef MIN_VERSION_ral+import qualified Data.RAList as RAL+#endif++sizes :: Num a => [a]+sizes = [100, 200, 500, 1000]+ main :: IO ()-main = defaultMain- [ bgroup "read/Chimera" (map benchReadChimera [100, 200, 500, 1000])- , bgroup "read/List" (map benchReadList [100, 200, 500, 1000])+main = defaultMain $ (: []) $ bgroup "read"+ [ bgroup "Chimera" (map benchReadChimera sizes)+ , bgroup "List" (map benchReadList sizes)+#ifdef MIN_VERSION_ral+ , bgroup "RAL" (map benchReadRAL sizes)+#endif ] randomChimera :: UChimera Int@@ -21,6 +33,11 @@ randomList :: [Int] randomList = randoms (mkStdGen 42) +#ifdef MIN_VERSION_ral+randomRAL :: RAL.RAList Int+randomRAL = RAL.fromList $ take (maximum sizes) $ randoms (mkStdGen 42)+#endif+ randomIndicesWord :: [Word] randomIndicesWord = randoms (mkStdGen 42) @@ -36,7 +53,18 @@ benchReadList :: Int -> Benchmark benchReadList n- = bench (show n)+ = bcompare ("$NF == \"" ++ show n ++ "\" && $(NF-1) == \"Chimera\"")+ $ bench (show n) $ nf (sum . map (randomList !!)) $ map (`mod` n) $ take n randomIndicesInt++#ifdef MIN_VERSION_ral+benchReadRAL :: Int -> Benchmark+benchReadRAL n+ = bcompare ("$NF == \"" ++ show n ++ "\" && $(NF-1) == \"Chimera\"")+ $ bench (show n)+ $ nf (sum . map (randomRAL RAL.!))+ $ map (`mod` n)+ $ take n randomIndicesInt+#endif
+ changelog.md view
@@ -0,0 +1,38 @@+# 0.3.2.0++* Implement `tabulateFix'`.+* Compatibility fixes.++# 0.3.1.0++* Define `Monad`, `MonadFix`, `MonadZip` instances.+* Define `Distributive` and `Representable` instances.+* Speed up `index`.++# 0.3.0.0++* Make `Chimera` polymorphic by vector type+* Implement `memoize` and `memoizeFix`.+* Implement `cycle` and `iterate`.+* Implement `mapSubvectors` and `zipSubvectors`+* Make boxed `tabulateFix` even lazier.+* Speed up `Data.Chimera.WheelMapping`.++# 0.2.0.0++* Generalize bit streams to `Chimera` datatype.+* Define `Applicative` instance.+* Implement `toList`, `trueIndices` and `falseIndices`.+* Make boxed `tabulateFix` lazier.++# 0.1.0.2++* Compatibility fixes.++# 0.1.0.1++* Compatibility fixes.++# 0.1.0.0++* Initial release.
chimera.cabal view
@@ -1,6 +1,6 @@ name: chimera-version: 0.3.1.0-cabal-version: >=1.10+version: 0.3.2.0+cabal-version: 2.0 build-type: Simple license: BSD3 license-file: LICENSE@@ -8,11 +8,12 @@ maintainer: andrew.lelechenko@gmail.com homepage: https://github.com/Bodigrim/chimera#readme category: Data-synopsis: Lazy infinite streams with O(1) indexing+synopsis: Lazy infinite streams with O(1) indexing and applications for memoization author: Bodigrim extra-source-files: README.md-tested-with: GHC==8.10.1, GHC==8.8.3, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2+ changelog.md+tested-with: GHC==9.0.1, GHC==8.10.5, GHC==8.8.4, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2 description: There are plenty of memoizing libraries on Hackage, but they usually fall into two categories:@@ -51,7 +52,6 @@ build-depends: base >=4.9 && <5, vector if flag(representable) build-depends: adjunctions, distributive, mtl- cpp-options: -DDefineRepresentable exposed-modules: Data.Chimera Data.Chimera.ContinuousMapping@@ -60,9 +60,9 @@ Data.Chimera.Compat Data.Chimera.FromIntegral default-language: Haskell2010- ghc-options: -Wall+ ghc-options: -Wall -Wcompat -test-suite test+test-suite chimera-test build-depends: base >=4.5 && <5, chimera,@@ -76,17 +76,17 @@ main-is: Test.hs default-language: Haskell2010 hs-source-dirs: test- ghc-options: -Wall+ ghc-options: -Wall -Wcompat -benchmark bench+benchmark chimera-bench build-depends: base, chimera,- gauge, mtl,- random+ random,+ tasty-bench >= 0.2.4 type: exitcode-stdio-1.0 main-is: Bench.hs default-language: Haskell2010 hs-source-dirs: bench- ghc-options: -Wall+ ghc-options: -Wall -Wcompat
test/Test.hs view
@@ -10,8 +10,8 @@ import Test.Tasty.QuickCheck as QC import Data.Bits+import Data.Foldable import Data.Function (fix)-import Data.List import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U @@ -43,7 +43,7 @@ ] , testGroup "to . from Z-curve 2D"- [ QC.testProperty "random" $ \z -> (\(x, y) -> toZCurve x y) (fromZCurve z) === z+ [ QC.testProperty "random" $ \z -> uncurry toZCurve (fromZCurve z) === z ] , testGroup "from . to Z-curve 2D" [ QC.testProperty "random" $ \x y -> fromZCurve (toZCurve x y) === (x `rem` (1 `shiftL` 32), y `rem` (1 `shiftL` 32))@@ -73,11 +73,18 @@ \(Fun _ (f :: Word -> Bool)) ix -> let jx = ix `mod` 65536 in f jx === Ch.index (Ch.tabulate f :: Ch.Chimera U.Vector Bool) jx+ , QC.testProperty "index . tabulateFix = fix" $ \(Fun _ g) ix -> let jx = ix `mod` 65536 in let f = mkUnfix g in fix f jx === Ch.index (Ch.tabulateFix f :: Ch.Chimera U.Vector Bool) jx++ , QC.testProperty "index . tabulateFix' = fix" $+ \(Fun _ g) ix ->+ let jx = ix `mod` 65536 in+ let f = mkUnfix g in+ fix f jx === Ch.index (Ch.tabulateFix' f :: Ch.Chimera U.Vector Bool) jx , QC.testProperty "iterate" $ \(Fun _ (f :: Word -> Word)) seed ix ->