diff --git a/core/src/main/scala/algebra/instances/bigInt.scala b/core/src/main/scala/algebra/instances/bigInt.scala index d6facbb7..49c1e512 100644 --- a/core/src/main/scala/algebra/instances/bigInt.scala +++ b/core/src/main/scala/algebra/instances/bigInt.scala @@ -2,15 +2,22 @@ package algebra package instances import algebra.ring._ +import cats.kernel.instances.BigIntOrder +import cats.kernel.{Hash, UnboundedEnumerable} package object bigInt extends BigIntInstances trait BigIntInstances extends cats.kernel.instances.BigIntInstances { - implicit val bigIntAlgebra: BigIntAlgebra = - new BigIntAlgebra + private val instance: BigIntAlgebra = new BigIntAlgebra + + implicit def bigIntAlgebra: EuclideanRing[BigInt] = instance + + implicit def bigIntTruncatedDivision: TruncatedDivision[BigInt] = instance } -class BigIntAlgebra extends CommutativeRing[BigInt] with Serializable { +class BigIntAlgebra extends EuclideanRing[BigInt] with TruncatedDivision.forCommutativeRing[BigInt] with Serializable { + + override def compare(x: BigInt, y: BigInt): Int = x.compare(y) val zero: BigInt = BigInt(0) val one: BigInt = BigInt(1) @@ -25,4 +32,36 @@ class BigIntAlgebra extends CommutativeRing[BigInt] with Serializable { override def fromInt(n: Int): BigInt = BigInt(n) override def fromBigInt(n: BigInt): BigInt = n + + def tquot(x: BigInt, y: BigInt): BigInt = x / y + def tmod(x: BigInt, y: BigInt): BigInt = x % y + override def tquotmod(x: BigInt, y: BigInt): (BigInt, BigInt) = x /% y + + 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 + } + } diff --git a/core/src/main/scala/algebra/ring/DivisionRing.scala b/core/src/main/scala/algebra/ring/DivisionRing.scala new file mode 100644 index 00000000..69de6743 --- /dev/null +++ b/core/src/main/scala/algebra/ring/DivisionRing.scala @@ -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) + +} + +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 + +} diff --git a/core/src/main/scala/algebra/ring/EuclideanRing.scala b/core/src/main/scala/algebra/ring/EuclideanRing.scala new file mode 100644 index 00000000..dbf0c9b8 --- /dev/null +++ b/core/src/main/scala/algebra/ring/EuclideanRing.scala @@ -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 => + 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) = { + if (EuclideanRing[A].isZero(b)) a else euclid(b, EuclideanRing[A].emod(a, b))*/ + +} diff --git a/core/src/main/scala/algebra/ring/Field.scala b/core/src/main/scala/algebra/ring/Field.scala index 9e51c450..d06b609b 100644 --- a/core/src/main/scala/algebra/ring/Field.scala +++ b/core/src/main/scala/algebra/ring/Field.scala @@ -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 @@ -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) } diff --git a/core/src/main/scala/algebra/ring/GCDRing.scala b/core/src/main/scala/algebra/ring/GCDRing.scala new file mode 100644 index 00000000..a2031f36 --- /dev/null +++ b/core/src/main/scala/algebra/ring/GCDRing.scala @@ -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 +} diff --git a/core/src/main/scala/algebra/ring/Signed.scala b/core/src/main/scala/algebra/ring/Signed.scala new file mode 100644 index 00000000..54f550b3 --- /dev/null +++ b/core/src/main/scala/algebra/ring/Signed.scala @@ -0,0 +1,141 @@ +package algebra +package ring + +import scala.{specialized => sp} + +/** + * A trait that expresses the existence of signs and absolute values on linearly ordered additive commutative monoids + * (i.e. types with addition and a zero). + * + * The following laws holds: + * + * (1) if `a <= b` then `a + c <= b + c` (linear order), + * (2) `signum(x) = -1` if `x < 0`, `signum(x) = 1` if `x > 0`, `signum(x) = 0` otherwise, + * + * Negative elements only appear when the scalar is taken from a additive abelian group. Then: + * + * (3) `abs(x) = -x` if `x < 0`, or `x` otherwise, + * + * Laws (1) and (2) lead to the triange inequality: + * + * (4) `abs(a + b) <= abs(a) + abs(b)` + * + * Signed should never be extended in implementations, rather the [[Signed.forAdditiveCommutativeMonoid]] and + * [[Signed.forAdditiveCommutativeGroup subtraits]]. + * + * It's better to have the Eq/PartialOrder/Order/Signed hierarchy separate from the Ring hierarchy, so that + * we do not end up with duplicate implicits. At the same time, we cannot use self-types to express + * the constraint that Signed must be an [[AdditiveCommutativeMonoid]], due to interaction with specialization. + */ +trait Signed[@sp(Byte, Short, Int, Long, Float, Double) A] extends Any with Order[A] { + + /** + * Returns Zero if `a` is 0, Positive if `a` is positive, and Negative is `a` is negative. + */ + def sign(a: A): Signed.Sign = Signed.Sign(signum(a)) + + /** + * Returns 0 if `a` is 0, 1 if `a` is positive, and -1 is `a` is negative. + */ + def signum(a: A): Int + + /** + * An idempotent function that ensures an object has a non-negative sign. + */ + def abs(a: A): A + + def isSignZero(a: A): Boolean = signum(a) == 0 + def isSignPositive(a: A): Boolean = signum(a) > 0 + def isSignNegative(a: A): Boolean = signum(a) < 0 + + def isSignNonZero(a: A): Boolean = signum(a) != 0 + def isSignNonPositive(a: A): Boolean = signum(a) <= 0 + def isSignNonNegative(a: A): Boolean = signum(a) >= 0 +} + +trait SignedFunctions[S[T] <: Signed[T]] extends cats.kernel.OrderFunctions[S] { + def sign[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Signed.Sign = + ev.sign(a) + def signum[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Int = + ev.signum(a) + def abs[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): A = + ev.abs(a) + def isSignZero[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignZero(a) + def isSignPositive[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignPositive(a) + def isSignNegative[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignNegative(a) + def isSignNonZero[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignNonZero(a) + def isSignNonPositive[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignNonPositive(a) + def isSignNonNegative[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: S[A]): Boolean = + ev.isSignNonNegative(a) +} + +object Signed extends SignedFunctions[Signed] { + + /** Signed implementation for additive commutative monoids */ + trait forAdditiveCommutativeMonoid[A] extends Any with Signed[A] with AdditiveCommutativeMonoid[A] { + def signum(a: A): Int = { + val c = compare(a, zero) + if (c < 0) -1 + else if (c > 0) 1 + else 0 + } + } + + /** Signed implementation for additive commutative groups */ + trait forAdditiveCommutativeGroup[A] extends Any with forAdditiveCommutativeMonoid[A] with AdditiveCommutativeGroup[A] { + def abs(a: A): A = if (compare(a, zero) < 0) negate(a) else a + } + + def apply[A](implicit s: Signed[A]): Signed[A] = s + + /** + * A simple ADT representing the `Sign` of an object. + */ + sealed abstract class Sign(val toInt: Int) { + def unary_- : Sign = this match { + case Positive => Negative + case Negative => Positive + case Zero => Zero + } + + def *(that: Sign): Sign = Sign(this.toInt * that.toInt) + + def **(that: Int): Sign = this match { + case Positive => Positive + case Zero if that == 0 => Positive + case Zero => Zero + case Negative if (that % 2) == 0 => Positive + case Negative => Negative + } + } + + case object Zero extends Sign(0) + case object Positive extends Sign(1) + case object Negative extends Sign(-1) + + object Sign { + implicit def sign2int(s: Sign): Int = s.toInt + + def apply(i: Int): Sign = + if (i == 0) Zero else if (i > 0) Positive else Negative + + private val instance: CommutativeMonoid[Sign] with MultiplicativeCommutativeMonoid[Sign] with Eq[Sign] = + new CommutativeMonoid[Sign] with MultiplicativeCommutativeMonoid[Sign] with Eq[Sign] { + def eqv(x: Sign, y: Sign): Boolean = x == y + def empty: Sign = Positive + def combine(x: Sign, y: Sign): Sign = x*y + def one: Sign = Positive + def times(x: Sign, y: Sign): Sign = x*y + } + + implicit final def signMultiplicativeMonoid: MultiplicativeCommutativeMonoid[Sign] = instance + implicit final def signMonoid: CommutativeMonoid[Sign] = instance + implicit final def signEq: Eq[Sign] = instance + } + +} diff --git a/core/src/main/scala/algebra/ring/TruncatedDivision.scala b/core/src/main/scala/algebra/ring/TruncatedDivision.scala new file mode 100644 index 00000000..8438952d --- /dev/null +++ b/core/src/main/scala/algebra/ring/TruncatedDivision.scala @@ -0,0 +1,87 @@ +package algebra +package ring + +import scala.{specialized => sp} + +/** + * Division and modulus for computer scientists + * taken from https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf + * + * For two numbers x (dividend) and y (divisor) on an ordered ring with y != 0, + * there exists a pair of numbers q (quotient) and r (remainder) + * such that these laws are satisfied: + * + * (1) q is an integer + * (2) x = y * q + r (division rule) + * (3) |r| < |y|, + * (4t) r = 0 or sign(r) = sign(x), + * (4f) r = 0 or sign(r) = sign(y). + * + * where sign is the sign function, and the absolute value + * function |x| is defined as |x| = x if x >=0, and |x| = -x otherwise. + * + * We define functions tmod and tquot such that: + * q = tquot(x, y) and r = tmod(x, y) obey rule (4t), + * (which truncates effectively towards zero) + * and functions fmod and fquot such that: + * q = fquot(x, y) and r = fmod(x, y) obey rule (4f) + * (which floors the quotient and effectively rounds towards negative infinity). + * + * Law (4t) corresponds to ISO C99 and Haskell's quot/rem. + * Law (4f) is described by Knuth and used by Haskell, + * and fmod corresponds to the REM function of the IEEE floating-point standard. + */ +trait TruncatedDivision[@sp(Byte, Short, Int, Long, Float, Double) A] extends Any with Signed[A] { + def tquot(x: A, y: A): A + def tmod(x: A, y: A): A + def tquotmod(x: A, y: A): (A, A) = (tquot(x, y), tmod(x, y)) + + def fquot(x: A, y: A): A + def fmod(x: A, y: A): A + def fquotmod(x: A, y: A): (A, A) = (fquot(x, y), fmod(x, y)) +} + +trait TruncatedDivisionFunctions[S[T] <: TruncatedDivision[T]] extends SignedFunctions[S] { + def tquot[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): A = + ev.tquot(x, y) + def tmod[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): A = + ev.tmod(x, y) + def tquotmod[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): (A, A) = + ev.tquotmod(x, y) + def fquot[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): A = + ev.fquot(x, y) + def fmod[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): A = + ev.fmod(x, y) + def fquotmod[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: TruncatedDivision[A]): (A, A) = + ev.fquotmod(x, y) +} + +object TruncatedDivision extends TruncatedDivisionFunctions[TruncatedDivision] { + trait forCommutativeRing[@sp(Byte, Short, Int, Long, Float, Double) A] + extends Any + with TruncatedDivision[A] + with Signed.forAdditiveCommutativeGroup[A] + with CommutativeRing[A] { self => + + def fmod(x: A, y: A): A = { + val tm = tmod(x, y) + if (signum(tm) == -signum(y)) plus(tm, y) else tm + } + + def fquot(x: A, y: A): A = { + val (tq, tm) = tquotmod(x, y) + if (signum(tm) == -signum(y)) minus(tq, one) else tq + } + + override def fquotmod(x: A, y: A): (A, A) = { + val (tq, tm) = tquotmod(x, y) + val signsDiffer = signum(tm) == -signum(y) + val fq = if (signsDiffer) minus(tq, one) else tq + val fm = if (signsDiffer) plus(tm, y) else tm + (fq, fm) + } + + } + + def apply[A](implicit ev: TruncatedDivision[A]): TruncatedDivision[A] = ev +} diff --git a/laws/shared/src/main/scala/algebra/laws/CombinationLaws.scala b/laws/shared/src/main/scala/algebra/laws/CombinationLaws.scala new file mode 100644 index 00000000..237b1e02 --- /dev/null +++ b/laws/shared/src/main/scala/algebra/laws/CombinationLaws.scala @@ -0,0 +1,59 @@ +package algebra.laws + +import cats.kernel._ +import org.typelevel.discipline.Laws +import org.scalacheck.{Arbitrary, Cogen, Prop} +import org.scalacheck.Prop._ +import cats.kernel.instances.all._ +import algebra.ring.{AdditiveCommutativeGroup, AdditiveCommutativeMonoid, GCDRing, Signed, TruncatedDivision} + +object CombinationLaws { + def apply[A: Eq: Arbitrary] = new CombinationLaws[A] { + def Equ: Eq[A] = Eq[A] + def Arb: Arbitrary[A] = implicitly[Arbitrary[A]] + } +} + +/** + * Contains laws that are obeying by combination of types, for example + * various kinds of signed rings. + */ +trait CombinationLaws[A] extends Laws { + + implicit def Equ: Eq[A] + implicit def Arb: Arbitrary[A] + + def signedAdditiveCommutativeMonoid(implicit signedA: Signed[A], A: AdditiveCommutativeMonoid[A]) = new DefaultRuleSet( + name = "signedAdditiveCMonoid", + parent = None, + "ordered group" -> forAll { (x: A, y: A, z: A) => + signedA.lteqv(x, y) ==> signedA.lteqv(A.plus(x, z), A.plus(y, z)) + }, + "triangle inequality" -> forAll { (x: A, y: A) => + signedA.lteqv(signedA.abs(A.plus(x, y)), A.plus(signedA.abs(x), signedA.abs(y))) + } + ) + + def signedAdditiveCommutativeGroup(implicit signedA: Signed[A], A: AdditiveCommutativeGroup[A]) = new DefaultRuleSet( + name = "signedAdditiveAbGroup", + parent = Some(signedAdditiveCommutativeMonoid), + "abs(x) equals abs(-x)" -> forAll { (x: A) => + signedA.abs(x) ?== signedA.abs(A.negate(x)) + } + ) + + // more a convention: as GCD is defined up to a unit, so up to a sign, + // on an ordered GCD ring we require gcd(x, y) >= 0, which is the common + // behavior of computer algebra systems + def signedGCDRing(implicit signedA: Signed[A], A: GCDRing[A]) = new DefaultRuleSet( + name = "signedGCDRing", + parent = Some(signedAdditiveCommutativeGroup), + "gcd(x, y) >= 0" -> forAll { (x: A, y: A) => + signedA.isSignNonNegative(A.gcd(x, y)) + }, + "gcd(x, 0) === abs(x)" -> forAll { (x: A) => + A.gcd(x, A.zero) ?== signedA.abs(x) + } + ) + +} diff --git a/laws/shared/src/main/scala/algebra/laws/OrderLaws.scala b/laws/shared/src/main/scala/algebra/laws/OrderLaws.scala index ad86191c..3e2dee0f 100644 --- a/laws/shared/src/main/scala/algebra/laws/OrderLaws.scala +++ b/laws/shared/src/main/scala/algebra/laws/OrderLaws.scala @@ -9,6 +9,8 @@ import org.scalacheck.Prop._ import cats.kernel.instances.all._ +import algebra.ring.{CommutativeRing, Signed, TruncatedDivision} + object OrderLaws { def apply[A: Eq: Arbitrary: Cogen]: OrderLaws[A] = new OrderLaws[A] { @@ -112,6 +114,75 @@ trait OrderLaws[A] extends Laws { } ) + def signed(implicit A: Signed[A]) = new OrderProperties( + name = "signed", + parent = Some(order), + "abs non-negative" -> forAll((x: A) => A.sign(A.abs(x)) != Signed.Negative), + "signum returns -1/0/1" -> forAll((x: A) => A.signum(A.abs(x)) <= 1), + "signum is sign.toInt" -> forAll((x: A) => A.signum(x) == A.sign(x).toInt) + ) + + def truncatedDivision(implicit ring: CommutativeRing[A], A: TruncatedDivision[A]) = new DefaultRuleSet( + name = "truncatedDivision", + parent = Some(signed), + "division rule (tquotmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val (q, r) = A.tquotmod(x, y) + x ?== ring.plus(ring.times(y, q), r) + } + }, + "division rule (fquotmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val (q, r) = A.fquotmod(x, y) + x ?== ring.plus(ring.times(y, q), r) + } + }, + "|r| < |y| (tmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val r = A.tmod(x, y) + A.lt(A.abs(r), A.abs(y)) + } + }, + "|r| < |y| (fmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val r = A.fmod(x, y) + A.lt(A.abs(r), A.abs(y)) + } + }, + "r = 0 or sign(r) = sign(x) (tmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val r = A.tmod(x, y) + A.isSignZero(r) || (A.sign(r) ?== A.sign(x)) + } + }, + "r = 0 or sign(r) = sign(y) (fmod)" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + val r = A.fmod(x, y) + A.isSignZero(r) || (A.sign(r) ?== A.sign(y)) + } + }, + "tquot" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + A.tquotmod(x, y)._1 ?== A.tquot(x, y) + } + }, + "tmod" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + A.tquotmod(x, y)._2 ?== A.tmod(x, y) + } + }, + "fquot" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + A.fquotmod(x, y)._1 ?== A.fquot(x, y) + } + }, + "fmod" -> forAll { (x: A, y: A) => + A.isSignNonZero(y) ==> { + A.fquotmod(x, y)._2 ?== A.fmod(x, y) + } + } + ) + class OrderProperties( name: String, parent: Option[RuleSet], diff --git a/laws/shared/src/main/scala/algebra/laws/RingLaws.scala b/laws/shared/src/main/scala/algebra/laws/RingLaws.scala index 16dfcc07..b5511915 100644 --- a/laws/shared/src/main/scala/algebra/laws/RingLaws.scala +++ b/laws/shared/src/main/scala/algebra/laws/RingLaws.scala @@ -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) => + (pred(x) && pred(y)) ==> { + A.euclideanFunction(x) <= A.euclideanFunction(A.times(x, y)) + } + } + ) + // boolean rings def boolRng(implicit A: BoolRng[A]) = RingProperties.fromParent( @@ -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, diff --git a/laws/shared/src/test/scala/algebra/laws/FPApprox.scala b/laws/shared/src/test/scala/algebra/laws/FPApprox.scala index 18b9e3ba..b9a076ab 100644 --- a/laws/shared/src/test/scala/algebra/laws/FPApprox.scala +++ b/laws/shared/src/test/scala/algebra/laws/FPApprox.scala @@ -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} @@ -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)) + } diff --git a/laws/shared/src/test/scala/algebra/laws/LawTests.scala b/laws/shared/src/test/scala/algebra/laws/LawTests.scala index 41c520a6..44593167 100644 --- a/laws/shared/src/test/scala/algebra/laws/LawTests.scala +++ b/laws/shared/src/test/scala/algebra/laws/LawTests.scala @@ -123,10 +123,12 @@ class LawTests extends munit.DisciplineSuite { checkAll("Long", RingLaws[Long].commutativeRing) checkAll("Long", LatticeLaws[Long].boundedDistributiveLattice) - checkAll("BigInt", RingLaws[BigInt].commutativeRing) + checkAll("BigInt", OrderLaws[BigInt].truncatedDivision) + checkAll("BigInt", RingLaws[BigInt].euclideanRing) + checkAll("BigInt", CombinationLaws[BigInt].signedGCDRing) - 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) {