Skip to content
This repository has been archived by the owner on Feb 8, 2022. It is now read-only.

Adding GCD rings and Euclidean rings, along with instances for BigInt #246

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion core/src/main/scala/algebra/instances/bigInt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ trait BigIntInstances extends cats.kernel.instances.BigIntInstances {
new BigIntAlgebra
}

class BigIntAlgebra extends CommutativeRing[BigInt] with Serializable {
class BigIntAlgebra extends EuclideanRing[BigInt] with Serializable {

val zero: BigInt = BigInt(0)
val one: BigInt = BigInt(1)
Expand All @@ -25,4 +25,32 @@ class BigIntAlgebra extends CommutativeRing[BigInt] with Serializable {

override def fromInt(n: Int): BigInt = BigInt(n)
override def fromBigInt(n: BigInt): BigInt = n

override def lcm(a: BigInt, b: BigInt)(implicit ev: Eq[BigInt]): BigInt =
if (a.signum == 0 || b.signum == 0) zero else (a / a.gcd(b)) * b
override def gcd(a: BigInt, b: BigInt)(implicit ev: Eq[BigInt]): BigInt = a.gcd(b)

def euclideanFunction(a: BigInt): BigInt = a.abs

override def equotmod(a: BigInt, b: BigInt): (BigInt, BigInt) = {
val (qt, rt) = a /% b // truncated quotient and remainder
if (rt.signum >= 0) (qt, rt)
else if (b.signum > 0) (qt - 1, rt + b)
else (qt + 1, rt - b)
}

def equot(a: BigInt, b: BigInt): BigInt = {
val (qt, rt) = a /% b // truncated quotient and remainder
if (rt.signum >= 0) qt
else if (b.signum > 0) qt - 1
else qt + 1
}

def emod(a: BigInt, b: BigInt): BigInt = {
val rt = a % b // truncated remainder
if (rt.signum >= 0) rt
else if (b > 0) rt + b
else rt - b
}

}
22 changes: 22 additions & 0 deletions core/src/main/scala/algebra/ring/DivisionRing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package algebra
package ring

import scala.{specialized => sp}

trait DivisionRing[@sp(Byte, Short, Int, Long, Float, Double) A] extends Any with Ring[A] with MultiplicativeGroup[A] {
self =>

def fromDouble(a: Double): A = Field.defaultFromDouble[A](a)(self, self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any law on this? Seems odd to me a division ring only has this extra method. We don't have any other division like operation?

Copy link
Contributor Author

@denisrosset denisrosset May 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field has it too. I should have fromDouble in DivisionRing and make Field inherit DivisionRing.

Basically, if you have a type A with a function BigInt -> A and division, you have fromDouble.

(Should algebra incorporate DivisionRing, or is it too specialized? Note that DivisionRing covers the quaternions)


}

trait DivisionRingFunctions[F[T] <: DivisionRing[T]] extends RingFunctions[F] with MultiplicativeGroupFunctions[F] {
def fromDouble[@sp(Int, Long, Float, Double) A](n: Double)(implicit ev: F[A]): A =
ev.fromDouble(n)
}

object DivisionRing extends DivisionRingFunctions[DivisionRing] {

@inline final def apply[A](implicit f: DivisionRing[A]): DivisionRing[A] = f

}
59 changes: 59 additions & 0 deletions core/src/main/scala/algebra/ring/EuclideanRing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package algebra
package ring

import scala.annotation.tailrec
import scala.{specialized => sp}

/**
* EuclideanRing implements a Euclidean domain.
*
* The formal definition says that every euclidean domain A has (at
* least one) euclidean function f: A -> N (the natural numbers) where:
*
* (for every x and non-zero y) x = yq + r, and r = 0 or f(r) < f(y).
*
* This generalizes the Euclidean division of integers, where f represents
* a measure of length (or absolute value), and the previous equation
* represents finding the quotient and remainder of x and y. So:
*
* quot(x, y) = q
* mod(x, y) = r
*/
trait EuclideanRing[@sp(Int, Long, Float, Double) A] extends Any with GCDRing[A] { self =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have specialized on these types can we also add the implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... Float and Double are taken charge of by Field (euclidean division is trivial).

For Byte/Short/Int/Long, the instances are not lawful as overflow is undefined behaviour territory (which was not the case for just CommutativeRing, the laws pass).

We could:

  1. Provide no instances
  2. Provide instances, but not test them
  3. Port the forAllSafe machinery from Spire that enables tests with abort-on-overflow

What do you think?

def euclideanFunction(a: A): BigInt
def equot(a: A, b: A): A
def emod(a: A, b: A): A
def equotmod(a: A, b: A): (A, A) = (equot(a, b), emod(a, b))
def gcd(a: A, b: A)(implicit ev: Eq[A]): A =
EuclideanRing.euclid(a, b)(ev, self)
def lcm(a: A, b: A)(implicit ev: Eq[A]): A =
if (isZero(a) || isZero(b)) zero else times(equot(a, gcd(a, b)), b)
// def xgcd(a: A, b: A)(implicit ev: Eq[A]): (A, A, A) =
}

trait EuclideanRingFunctions[R[T] <: EuclideanRing[T]] extends GCDRingFunctions[R] {
def euclideanFunction[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: R[A]): BigInt =
ev.euclideanFunction(a)
def equot[@sp(Int, Long, Float, Double) A](a: A, b: A)(implicit ev: R[A]): A =
ev.equot(a, b)
def emod[@sp(Int, Long, Float, Double) A](a: A, b: A)(implicit ev: R[A]): A =
ev.emod(a, b)
def equotmod[@sp(Int, Long, Float, Double) A](a: A, b: A)(implicit ev: R[A]): (A, A) =
ev.equotmod(a, b)
}

object EuclideanRing extends EuclideanRingFunctions[EuclideanRing] {

@inline final def apply[A](implicit e: EuclideanRing[A]): EuclideanRing[A] = e

/**
* Simple implementation of Euclid's algorithm for gcd
*/
@tailrec final def euclid[@sp(Int, Long, Float, Double) A: Eq: EuclideanRing](a: A, b: A): A = {
if (EuclideanRing[A].isZero(b)) a else euclid(b, EuclideanRing[A].emod(a, b))
}

/* @tailrec final def extendedEuclid[@sp(Int, Long, Float, Double) A: Eq: EuclideanRing](a: A, b: A): (A, A, A) = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Or I'll implement the extended GCD algorithm.

if (EuclideanRing[A].isZero(b)) a else euclid(b, EuclideanRing[A].emod(a, b))*/

}
17 changes: 15 additions & 2 deletions core/src/main/scala/algebra/ring/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ package ring

import scala.{ specialized => sp }

trait Field[@sp(Int, Long, Float, Double) A] extends Any with CommutativeRing[A] with MultiplicativeCommutativeGroup[A] { self =>
trait Field[@sp(Int, Long, Float, Double) A] extends Any with EuclideanRing[A] with MultiplicativeCommutativeGroup[A] { self =>

// default implementations for GCD

override def gcd(a: A, b: A)(implicit eqA: Eq[A]): A =
if (isZero(a) && isZero(b)) zero else one
override def lcm(a: A, b: A)(implicit eqA: Eq[A]): A = times(a, b)

// default implementations for Euclidean division in a field (as every nonzero element is a unit!)

def euclideanFunction(a: A): BigInt = BigInt(0)
def equot(a: A, b: A): A = div(a, b)
def emod(a: A, b: A): A = zero
override def equotmod(a: A, b: A): (A, A) = (div(a, b), zero)

/**
* This is implemented in terms of basic Field ops. However, this is
Expand All @@ -17,7 +30,7 @@ trait Field[@sp(Int, Long, Float, Double) A] extends Any with CommutativeRing[A]

}

trait FieldFunctions[F[T] <: Field[T]] extends RingFunctions[F] with MultiplicativeGroupFunctions[F] {
trait FieldFunctions[F[T] <: Field[T]] extends EuclideanRingFunctions[F] with MultiplicativeGroupFunctions[F] {
def fromDouble[@sp(Int, Long, Float, Double) A](n: Double)(implicit ev: F[A]): A =
ev.fromDouble(n)
}
Expand Down
41 changes: 41 additions & 0 deletions core/src/main/scala/algebra/ring/GCDRing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package algebra
package ring

import scala.{specialized => sp}

/**
* GCDRing implements a GCD ring.
*
* For two elements x and y in a GCD ring, we can choose two elements d and m
* such that:
*
* d = gcd(x, y)
* m = lcm(x, y)
*
* d * m = x * y
*
* Additionally, we require:
*
* gcd(0, 0) = 0
* lcm(x, 0) = lcm(0, x) = 0
*
* and commutativity:
*
* gcd(x, y) = gcd(y, x)
* lcm(x, y) = lcm(y, x)
*/
trait GCDRing[@sp(Int, Long, Float, Double) A] extends Any with CommutativeRing[A] {
def gcd(a: A, b: A)(implicit ev: Eq[A]): A
def lcm(a: A, b: A)(implicit ev: Eq[A]): A
}

trait GCDRingFunctions[R[T] <: GCDRing[T]] extends RingFunctions[R] {
def gcd[@sp(Int, Long, Float, Double) A](a: A, b: A)(implicit ev: R[A], eqA: Eq[A]): A =
ev.gcd(a, b)(eqA)
def lcm[@sp(Int, Long, Float, Double) A](a: A, b: A)(implicit ev: R[A], eqA: Eq[A]): A =
ev.lcm(a, b)(eqA)
}

object GCDRing extends GCDRingFunctions[GCDRing] {
@inline final def apply[A](implicit ev: GCDRing[A]): GCDRing[A] = ev
}
76 changes: 76 additions & 0 deletions laws/shared/src/main/scala/algebra/laws/RingLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,57 @@ trait RingLaws[A] extends GroupLaws[A] { self =>
parents = Seq(ring, commutativeRig, commutativeRng)
)

def gcdRing(implicit A: GCDRing[A]) = RingProperties.fromParent(
name = "gcd domain",
parent = commutativeRing,
"gcd/lcm" -> forAll { (x: A, y: A) =>
val d = A.gcd(x, y)
val m = A.lcm(x, y)
A.times(x, y) ?== A.times(d, m)
},
"gcd is commutative" -> forAll { (x: A, y: A) =>
A.gcd(x, y) ?== A.gcd(y, x)
},
"lcm is commutative" -> forAll { (x: A, y: A) =>
A.lcm(x, y) ?== A.lcm(y, x)
},
"gcd(0, 0)" -> (A.gcd(A.zero, A.zero) ?== A.zero),
"lcm(0, 0) === 0" -> (A.lcm(A.zero, A.zero) ?== A.zero),
"lcm(x, 0) === 0" -> forAll { (x: A) => A.lcm(x, A.zero) ?== A.zero }
)

def euclideanRing(implicit A: EuclideanRing[A]) = RingProperties.fromParent(
name = "euclidean ring",
parent = gcdRing,
"euclidean division rule" -> forAll { (x: A, y: A) =>
pred(y) ==> {
val (q, r) = A.equotmod(x, y)
x ?== A.plus(A.times(y, q), r)
}
},
"equot" -> forAll { (x: A, y: A) =>
pred(y) ==> {
A.equotmod(x, y)._1 ?== A.equot(x, y)
}
},
"emod" -> forAll { (x: A, y: A) =>
pred(y) ==> {
A.equotmod(x, y)._2 ?== A.emod(x, y)
}
},
"euclidean function" -> forAll { (x: A, y: A) =>
pred(y) ==> {
val (_, r) = A.equotmod(x, y)
A.isZero(r) || (A.euclideanFunction(r) < A.euclideanFunction(y))
}
},
"submultiplicative function" -> forAll { (x: A, y: A) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see this comment in trait. Could we add it so people get more intuition for the euclideanFunction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

(pred(x) && pred(y)) ==> {
A.euclideanFunction(x) <= A.euclideanFunction(A.times(x, y))
}
}
)

// boolean rings

def boolRng(implicit A: BoolRng[A]) = RingProperties.fromParent(
Expand All @@ -227,6 +278,31 @@ trait RingLaws[A] extends GroupLaws[A] { self =>
// zero * x == x * zero hold.
// Luckily, these follow from the other field and group axioms.
def field(implicit A: Field[A]) = new RingProperties(
name = "field",
al = additiveCommutativeGroup,
ml = multiplicativeCommutativeGroup,
parents = Seq(euclideanRing),
"fromDouble" -> forAll { (n: Double) =>
if (Platform.isJvm) {
// TODO: BigDecimal(n) is busted in scalajs, so we skip this test.
val bd = new java.math.BigDecimal(n)
val unscaledValue = new BigInt(bd.unscaledValue)
val expected =
if (bd.scale > 0) {
A.div(A.fromBigInt(unscaledValue), A.fromBigInt(BigInt(10).pow(bd.scale)))
} else {
A.fromBigInt(unscaledValue * BigInt(10).pow(-bd.scale))
}
Field.fromDouble[A](n) ?== expected
} else {
Prop(true)
}
}
)

// Approximate fields such a Float or Double, even through filtered using FPFilter, do not work well with
// Euclidean ring tests
def approxField(implicit A: Field[A]) = new RingProperties(
name = "field",
al = additiveCommutativeGroup,
ml = multiplicativeCommutativeGroup,
Expand Down
3 changes: 3 additions & 0 deletions laws/shared/src/test/scala/algebra/laws/FPApprox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import algebra.ring._
* equal to 0.1, then it's plausible they could be equal to each other, so we
* return true. On the other hand, if the error bound is less than 0.1, then we
* can definitely say they cannot be equal to each other.
*
* Based on https://dl.acm.org/doi/10.1145/276884.276904
*/
case class FPApprox[A](approx: A, mes: A, ind: BigInt) {
import FPApprox.{abs, Epsilon}
Expand Down Expand Up @@ -142,4 +144,5 @@ class FPApproxAlgebra[A: Order: FPApprox.Epsilon](implicit ev: Field[A]) extends
override def fromInt(x: Int): FPApprox[A] = FPApprox.approx(ev.fromInt(x))
override def fromBigInt(x: BigInt): FPApprox[A] = FPApprox.approx(ev.fromBigInt(x))
override def fromDouble(x: Double): FPApprox[A] = FPApprox.approx(ev.fromDouble(x))

}
6 changes: 3 additions & 3 deletions laws/shared/src/test/scala/algebra/laws/LawTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ class LawTests extends munit.DisciplineSuite {
checkAll("Long", RingLaws[Long].commutativeRing)
checkAll("Long", LatticeLaws[Long].boundedDistributiveLattice)

checkAll("BigInt", RingLaws[BigInt].commutativeRing)
checkAll("BigInt", RingLaws[BigInt].euclideanRing)

checkAll("FPApprox[Float]", RingLaws[FPApprox[Float]].field)
checkAll("FPApprox[Double]", RingLaws[FPApprox[Double]].field)
checkAll("FPApprox[Float]", RingLaws[FPApprox[Float]].approxField)
checkAll("FPApprox[Double]", RingLaws[FPApprox[Double]].approxField)

// let's limit our BigDecimal-related tests to the JVM for now.
if (Platform.isJvm) {
Expand Down