"Die ganzen Zahlen hat der liebe Gott gemacht, alles andere ist Menschenwerk."
-- Leopold Kronecker
(God made the integers, everything else is human work.)
Kronecker is intended to be a library for enumerating all distinct values of types.
The Countable[A]
type class provides a get
method, which takes an
index (given as a spire.math.SafeLong
) and returns an Option[A]
.
The type class instance must be able to generate any possible A
value (provided a large enough input number is provided).
Here's an example REPL session that demonstrates some basic functionality:
import kronecker._
val c0 = Countable[List[Byte]]
c0.get(0) // Some(List())
c0.get(1) // Some(List(0))
c0.get(2) // Some(List(1))
c0.get(1000) // Some(List(2, -25))
c0.get(1000000) // Some(List(14, 65, 63))
c0.get(1000000000) // Some(List(58, -103, -56, -1))
val i = Z(20).pow(64)
// 184467440737095516160000000000000000000000000000000000000000000000000000000000000000
c0.get(i)
// Some(List(23, 78, 2, -24, 62, -8, -13, -39, -90, -106, -20, 109, 55,
// -20, 99, -66, 105, 29, -1, -2, -2, -2, -2, -2, -2, -2, -2,
// -2, -2, -2, -2, -2, -2, -2, -1))
val c1 = Countable[(Z, Z, Z, Z, Z, Z, Z, Z, Z)]
c1.get(0) // Some((0,0,0,0,0,0,0,0,0))
c1.get(1) // Some((1,0,0,0,0,0,0,0,0))
c1.get(2) // Some((0,1,0,0,0,0,0,0,0))
c1.get(1000) // Some((1,0,-2,0,0,0,0,0,0))
c1.get(1000000) // Some((-1,0,2,1,2,-1,0,0,-2))
c1.get(1000000000) // Some((2,1,0,4,-5,-1,-2,3,3))
val j = Z(20).pow(32)
// 429496729600000000000000000000000000000000
c1.get(j) // Some((25689,-10621,1664,-1809,-14874,22207,-1114,9471,172))
After the JVM is warm, the above examples run in near-instant time on
the author's machine. However it's not hard to increment the exponents
used until the library takes "too long" to return (for example,
c1.get(i)
takes longer than you'll care to wait).
The laws for ev: Finite[A]
are as follows:
ev.size >= 0
ev.get(i)
returnsSome(_)
for alli
in[0, ev.size)
ev.get(i)
returnsNone
for alli >= ev.size
ev.get(i) = Some(_) = ev.get(j)
if and only ifi == j
- for every
a: A
there is ani
such thatev.get(i) = Some(a)
ev.cardinality = Card(ev.size)
The laws for ev: Infinite[A]
are as follows:
ev(i) = ev(j)
if and only ifi == j
- for every
a: A
there is ani
such thatev(i) = a
ev.get(i) = Some(ev(i))
ev.cardinality = Infinite
Every Countable[A]
is either Finite[A]
or Infinite[A]
, so the
"common" laws that apply no matter what are:
ev.get(i)
returnsSome(_)
for alli < ev.cardinality
ev.get(i)
returnsNone
for alli >= ev.cardinality
ev.get(i) = Some(_) = ev.get(j)
if and only ifi == j
- for every
a: A
there is ani
such thatev.get(i) = Some(a)
In all these laws i
is assumed to be a non-negative, unbounded
integer (i.e. a spire.math.SafeLong
, aliased as Z
in Kronecker).
Some people might take issue with the finite/infinite terminology
(especially in a library named for Kronecker). The terms stand in for
bounded/unbounded. Finite[A]
instances have a hard upper bound on
how many unique values exist, regardless of how much work the caller
chooses to do generating them. By contrast, Infinite[A]
instances
can (in principle) go as big as the caller desires (given adequte time
and space).
In practice, there are certain internal optimizations which lower the theoretical bounds somewhat, but the authors are confident that most users will be unable to observe these in practice.
There are numbers that are too big to compute arithmetically and which will cause your JVM to appear to hang. Kronecker's API may encourage you to dance dangerously close to the chasm of non-termination: consider yourself warned.
Currently Finite[Set[A]]
can fail if the corresponding Finite[A]
has a cardinality that is too large. The work-around is to create a
bogus infinite instance as follows:
def bogus[A](implicit ev: Finite[A]): Infinite[Set[A]] = {
val ia: Infinite[A] = Countable.sz.translate(ev.get(_).get)
Countable.iset(ia)
}
This issue will be shared by any other instances that are technically finite but so large that we can't arithmetically represent their cardinality.
It might nice to add a co-countable type class (i.e. Indexable
) to
represent being able to go from a value to its index (A => Z
). This
would be analogous to the relationship between Gen
and Cogen
;
using Indexable
we could provide Countable[A => B]
instances.
We could generalize our support for Countable[Either[A, B]]
to
coproducts (e.g. Countable[A :+: B :+: ...]
) in the same way we
currently support HList
. With this, we could add support for
deriving sealed ADTs.
It's possible that exposing the finite/infinite split to the user is not the best design. It would be possible to paper over this with pattern-matching (although we'd lose some assurances we currently get about doing things properly).
There is some work-in-progress around actually using Countable[A] to power property-based tests (e.g. ScalaCheck). This might end up being kind of cool if it works.
In terms of space-age work, using some model of codata to bound how much work we're willing to do (and to catch situations where the JVM might appear to hang before it does so) would be pretty radical.
All code is available to you under the Apache 2 license, available at https://opensource.org/licenses/Apache-2.0.
Copyright Erik Osheim, 2018.