module Data.Int.Base where
The integersđ
The familiar set of integers, in addition to its intuitive characterisation in terms of positive and negative numbers, can be specified by a wide variety of universal properties:
The set of integers, with the operation of addition, is the free group on one generator (see the group of integers).
The set of integers, with the operations of addition and multiplication, is the initial ring.
The set of integers under addition, considered together with the embedding of the positive numbers, is the free group generated by the monoid of natural numbers.
The set of integers is the loop space of the circle. More generally, it appears (as a group under addition) as the first non-trivial higher homotopy group of all spheres:
The set of integers, with the number zero and the successor equivalence, is the initial pointed type with an equivalence.
All of these specifications can be turned into code, and regardless of your choice, it would be provably equivalent to any of the others. Therefore, there is no mathematical reason to pick one over the others. However, the real world imposes two constraints on us: convenience and efficiency.
On the convenience front, itâs simply more convenient to formalise further results if there is a definition of the integers which we take as the definition. And, since this definition will be ubiquitous, itâs best if it has compact normal forms for numbers, and supports definition of the relevant structure in a computationally nice way.
For definiteness, we go with the most elementary, inductive representation:
data Int : Type where pos : Nat â Int negsuc : Nat â Int
{-# BUILTIN INTEGER Int #-} {-# BUILTIN INTEGERPOS pos #-} {-# BUILTIN INTEGERNEGSUC negsuc #-} pattern posz = pos zero pattern possuc x = pos (suc x)
The definition above is isomorphic to a sum type, where both summands are the natural numbers. However, if we interpret this naĂŻvely, then we would have a problem: there are now two copies of the number zero! This is, essentially, a problem of intent. We have to choose one of the two summands to contain the number zero, and the names we choose for the constructors must reflect this.
The constructor pos
embeds the positive
numbers â incl. zero! â while the constructor negsuc
constructs the
negation of a successor. This means
that negsuc 0
is the
representation of the number
Other than these constructors, we can define a difference operation, between natural numbers, valued in the integers. This difference correctly reflects, in its sign, whether we tried to subtract a large quantity from a smaller quantity.
_â-_ : Nat â Nat â Int x â- zero = pos x zero â- suc y = negsuc y suc x â- suc y = x â- y
We can also use this to demonstrate the offsetting built into negsuc
:
_ : 0 â- 20 ⥠negsuc 19 _ = refl
pos-injective : â {x y} â pos x ⥠pos y â x ⥠y pos-injective = ap (case_of λ { (pos x) â x ; (negsuc _) â zero }) negsuc-injective : â {x y} â negsuc x ⥠negsuc y â x ⥠y negsuc-injective = ap (case_of λ { (pos x) â 0 ; (negsuc x) â x }) posâ negsuc : â {x y} â pos x ⥠negsuc y â â„ posâ negsuc p = subst (λ { (pos x) â †; (negsuc _) â â„ }) p tt negsucâ pos : â {x y} â negsuc x ⥠pos y â â„ negsucâ pos p = posâ negsuc (sym p) _ = Discrete-Nat
Equalityđ
We mentioned in the introductory paragraph that the type of integers is a set. We will show something stronger: itâs actually discrete. This means that we have a procedure that can tell whether two integers are equal, and produce a refutation when they are not equal. Intuitively, this is because the natural numbers are discrete, and itâs embedded in the integers.
The first thing to do is discriminate between the two constructors. If they match, we can compare the underlying natural numbers:
instance Discrete-Int : Discrete Int Discrete-Int {pos x} {pos y} with x âĄ? y ... | yes p = yes (ap pos p) ... | no ÂŹp = no λ path â ÂŹp (pos-injective path) Discrete-Int {negsuc x} {negsuc y} with x âĄ? y ... | yes p = yes (ap negsuc p) ... | no ÂŹp = no λ path â ÂŹp (negsuc-injective path)
If theyâre mismatched, we have pre-existing refutations.
Discrete-Int {pos x} {negsuc y} = no posâ negsuc Discrete-Int {negsuc x} {pos y} = no negsucâ pos
Number-Int : Number Int Number-Int .Number.Constraint _ = †Number-Int .Number.fromNat n = pos n Negative-Int : Negative Int Negative-Int .Negative.Constraint _ = †Negative-Int .Negative.fromNeg zero = pos zero Negative-Int .Negative.fromNeg (suc x) = negsuc x opaque H-Level-Int : â {n} â H-Level Int (2 + n) H-Level-Int = basic-instance 2 (Discreteâis-set Discrete-Int)
As the universal symmetryđ
One of the mentioned characterisations of the integers was as the initial type equipped with a point and an auto-equivalence. This equivalence is the successor function: if we picture the integers as a number line, the effect of this equivalence is to ârotateâ the line to the right.
suc†: Int â Int suc†(pos n) = pos (suc n) suc†(negsuc zero) = 0 suc†(negsuc (suc n)) = negsuc n pred†: Int â Int pred†(pos zero) = -1 pred†(pos (suc n)) = pos n pred†(negsuc n) = negsuc (suc n)
The definition of the successor and predecessor functions is slightly complicated by the need to adjust by one when passing between the summands. The proof that these are inverses is a case bash precisely mirroring the structure of the functions.
suc-pred†: (x : Int) â suc†(pred†x) ⥠x suc-pred†(negsuc x) = refl suc-pred†(pos zero) = refl suc-pred†(pos (suc x)) = refl pred-suc†: (x : Int) â pred†(suc†x) ⥠x pred-suc†(pos x) = refl pred-suc†(negsuc zero) = refl pred-suc†(negsuc (suc x)) = refl suc-equiv : Int â Int suc-equiv = IsoâEquiv (suc†, iso pred†suc-pred†pred-sucâ€)
As signed numbersđ
Considering the isomorphism we arrive at an equivalent representation of the integers: as a pair consisting of a natural number and its sign. We have projections and which correspond to this view: given an integer, we can determine its sign and its absolute value.
data Sign : Type where pos neg : Sign abs : Int â Nat abs (pos x) = x abs (negsuc x) = suc x sign : Int â Sign sign (pos x) = pos sign (negsuc x) = neg
Conversely, if we have a signed number, we can build an integer. Note
that the assign
function sends the natural number zero to the integer zero regardless of
what sign is specified.
assign : Sign â Nat â Int assign s zero = 0 assign pos (suc n) = pos (suc n) assign neg (suc n) = negsuc n
Algebrađ
We also mentioned two more characterisations of the integers: as the free group on one generator, and as the initial ring. Therefore, we expect to find operations of addition, multiplication, and negation (additive inverse) on the set of integers. They are not that hard to define.
Addition on integers is defined by cases on the sign. If both numbers are positive (resp. negative), then we can compute their sum in the natural numbers. If the numbers have mismatched sign, then the addition function is actually computing a difference, and we already know how to compute differences.
_+â€_ : Int â Int â Int pos x +†pos y = pos (x + y) pos x +†negsuc y = x â- suc y negsuc x +†pos y = y â- suc x negsuc x +†negsuc y = negsuc (suc (x + y))
The negation function is defined by a short case bash. Subtraction is defined as addition against the inverse, rather than as an operation in its own right.
neg†: Int â Int neg†(pos zero) = pos zero neg†(pos (suc x)) = negsuc x neg†(negsuc x) = pos (suc x) _-â€_ : Int â Int â Int x -†y = x +†(neg†y)
The implementation of multiplication uses the decomposition of numbers into their signs and absolute values: The product is defined to be
There are actually three different âmultiplicationâ signs in the
formula above. The first is sign multiplication, the second is
assign
, and the last is
natural number multiplication.
signĂ : Sign â Sign â Sign signĂ pos pos = pos signĂ neg neg = pos signĂ pos neg = neg signĂ neg pos = neg _*â€_ : Int â Int â Int i@(pos _) *†j = assign (signĂ pos (sign j)) (abs i * abs j) i@(negsuc _) *†j = assign (signĂ neg (sign j)) (abs i * abs j)
There are actually alternative definitions of addition and multiplication: as iterated successor/predecessor, and as iterated addition, respectively. These alternative representations have worse performance, but they behave in a way that is slightly easier to reason about. When establishing the algebraic properties of the integers, weâll prove that these functions are equivalent to the definitions above, and change between them as appropriate.
rot†: Int â Int â Int rot†(pos zero) y = y rot†(pos (suc x)) y = suc†(rot†(pos x) y) rot†(negsuc zero) y = pred†y rot†(negsuc (suc x)) y = pred†(rot†(negsuc x) y) dot†: Int â Int â Int dot†posz y = posz dot†(possuc x) y = y +†(dot†(pos x) y) dot†(negsuc zero) y = neg†y dot†(negsuc (suc x)) y = neg†y +†(dot†(negsuc x) y)
Additional operationsđ
It is also straightforward to define maximum and minimum operations for integers:
max†: Int â Int â Int max†(pos x) (pos y) = pos (max x y) max†(pos x) (negsuc _) = pos x max†(negsuc _) (pos y) = pos y max†(negsuc x) (negsuc y) = negsuc (min x y) min†: Int â Int â Int min†(pos x) (pos y) = pos (min x y) min†(pos _) (negsuc y) = negsuc y min†(negsuc x) (pos _) = negsuc x min†(negsuc x) (negsuc y) = negsuc (max x y)
data Positive : Int â Type where pos : â x â Positive (possuc x) instance H-Level-Positive : â {n k} â H-Level (Positive n) (suc k) H-Level-Positive {n} = prop-instance (retractâis-hlevel 1 (from _) (to _) (li _) (isp n)) where pos' : Int â Type pos' posz = â„ pos' (possuc x) = †pos' (negsuc x) = â„ isp : â n â is-prop (pos' n) isp (possuc _) tt tt = refl to : â x â Positive x â pos' x to .(possuc x) (pos x) = tt from : â x â pos' x â Positive x from (possuc x) tt = pos x li : â x â is-left-inverse (from x) (to x) li .(possuc x) (pos x) = refl Positive-suc : â {x} â Positive (possuc x) Positive-suc = pos _ Dec-Positive : â {x} â Dec (Positive x) Dec-Positive {posz} = no λ () Dec-Positive {negsuc x} = no λ () Dec-Positive {possuc x} = yes (pos x)