packages feed

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 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 [![Hackage](http://img.shields.io/hackage/v/chimera.svg)](https://hackage.haskell.org/package/chimera) [![Stackage LTS](http://stackage.org/package/chimera/badge/lts)](http://stackage.org/lts/package/chimera) [![Stackage Nightly](http://stackage.org/package/chimera/badge/nightly)](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 ->