chimera
Lazy infinite streams with O(1) indexing
https://github.com/Bodigrim/chimera#readme
Version on this page: | 0.3.1.0 |
LTS Haskell 23.14: | 0.4.1.0 |
Stackage Nightly 2025-03-16: | 0.4.1.0 |
Latest on Hackage: | 0.4.1.0 |
chimera-0.3.1.0@sha256:1cacfe53e2ce9eda0eec6ae8b51af6c5626e0202333c08080f87d30bf9ed8b19,2525
Module documentation for 0.3.1.0
chimera
Lazy infinite compact streams with cache-friendly O(1) indexing and applications for memoization.
Imagine having a function f :: Word -> a
,
which is expensive to evaluate. We would like to memoize it,
returning g :: Word -> a
, which does effectively the same,
but transparently caches results to speed up repetitive
re-evaluation.
There are plenty of memoizing libraries on Hackage, but they usually fall into two categories:
-
Store cache as a flat array, enabling us to obtain cached values in O(1) time, which is nice. The drawback is that one must specify the size of the array beforehand, limiting an interval of inputs, and actually allocate it at once.
-
Store cache as a lazy binary tree. Thanks to laziness, one can freely use the full range of inputs. The drawback is that obtaining values from a tree takes logarithmic time and is unfriendly to CPU cache, which kinda defeats the purpose.
This package intends to tackle both issues,
providing a data type Chimera
for
lazy infinite compact streams with cache-friendly O(1) indexing.
Additional features include:
- memoization of recursive functions and recurrent sequences,
- memoization of functions of several, possibly signed arguments,
- efficient memoization of boolean predicates.
Example 1
Consider the following predicate:
isOdd :: Word -> Bool
isOdd n = if n == 0 then False else not (isOdd (n - 1))
Its computation is expensive, so we’d like to memoize it:
isOdd' :: Word -> Bool
isOdd' = memoize isOdd
This is fine to avoid re-evaluation for the same arguments.
But isOdd
does not use this cache internally, going all the way
of recursive calls to n = 0
. We can do better,
if we rewrite isOdd
as a fix
point of isOddF
:
isOddF :: (Word -> Bool) -> Word -> Bool
isOddF f n = if n == 0 then False else not (f (n - 1))
and invoke memoizeFix
to pass cache into recursive calls as well:
isOdd' :: Word -> Bool
isOdd' = memoizeFix isOddF
Example 2
Define a predicate, which checks whether its argument is a prime number, using trial division.
isPrime :: Word -> Bool
isPrime n = n > 1 && and [ n `rem` d /= 0 | d <- [2 .. floor (sqrt (fromIntegral n))], isPrime d]
This is certainly an expensive recursive computation and we would like
to speed up its evaluation by wrappping into a caching layer.
Convert the predicate to an unfixed form such that isPrime = fix isPrimeF
:
isPrimeF :: (Word -> Bool) -> Word -> Bool
isPrimeF f n = n > 1 && and [ n `rem` d /= 0 | d <- [2 .. floor (sqrt (fromIntegral n))], f d]
Now create its memoized version for rapid evaluation:
isPrime' :: Word -> Bool
isPrime' = memoizeFix isPrimeF
Magic and its exposure
Internally Chimera
is represented as a boxed vector
of growing (possibly, unboxed) vectors v a
:
newtype Chimera v a = Chimera (Data.Vector.Vector (v a))
Assuming 64-bit architecture, the outer vector consists of 65 inner vectors of sizes 1, 1, 2, 22, …, 263. Since the outer vector is boxed, inner vectors are allocated on-demand only: quite fortunately, there is no need to allocate all 264 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,
can be done instantly by ffs
instruction. The caveat here is
that accessing an inner vector first time will cause its allocation,
taking O(n) time. So to restore amortized O(1) time we must assume
a dense access. Chimera
is no good for sparse access
over a thin set of indices.
One can argue that this structure is not infinite,
because it cannot handle more than 264 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,
one could use more layers of indirection, raising access time
to O(log* 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).