coulomb-refined

The coulomb-refined package defines policies and utilities for integrating the refined typelevel libraries with coulomb.

Quick Start

documentation

You can browse the coulomb-refined policies here.

packages

Include coulomb-refined with your Scala project:

libraryDependencies += "com.manyangled" %% "coulomb-core" % "0.8.0"
libraryDependencies += "com.manyangled" %% "coulomb-refined" % "0.8.0"

import

To import the standard coulomb policy with the refined overlay:

// fundamental coulomb types and methods
import coulomb.*
import coulomb.syntax.*

// common refined definitions
import eu.timepit.refined.*
import eu.timepit.refined.api.*
import eu.timepit.refined.numeric.*

// algebraic definitions
import algebra.instances.all.given
import coulomb.ops.algebra.all.{*, given}

// standard policy for spire and scala types
import coulomb.policy.standard.given
import scala.language.implicitConversions

// overlay policy for refined integrations
import coulomb.policy.overlay.refined.algebraic.given

// coulomb syntax for refined integrations
import coulomb.syntax.refined.*

examples

Examples in this section will use the following workaround as a replacement for refineMV until it is ported forward to Scala 3.

// a workaround for refineMV not being available in scala3
// https://github.com/fthomas/refined/issues/932
object workaround:
    extension [V](v: V)
        def withRP[P](using Validate[V, P]): Refined[V, P] =
            refineV[P].unsafeFrom(v)

import workaround.*

The coulomb-refined package supports refined predicates that are algebraically well-behaved for applicable operations. Primarily this means the predicates Positive and NonNegative. For example, the positive doubles are an additive semigroup and multiplicative group, as the following code demonstrates.

The table below summarizes the full list of supported refined predicates and associated algebras.

import coulomb.units.si.{*, given}
import coulomb.units.us.{*, given}

val pos1 = 1d.withRP[Positive].withUnit[Meter]
// pos1: Quantity[Refined[Double, Positive], Meter] = 1.0
val pos2 = 2d.withRP[Positive].withUnit[Meter]
// pos2: Quantity[Refined[Double, Positive], Meter] = 2.0
val pos3 = 3d.withRP[Positive].withUnit[Second]
// pos3: Quantity[Refined[Double, Positive], Second] = 3.0

// positive doubles are an additive semigroup
pos1 + pos2
// res0: Quantity[Refined[Double, Positive], Meter] = 3.0

// also a multiplicative semigroup
pos1 * pos2
// res1: Quantity[Refined[Double, Positive], ^[Meter, 2]] = 2.0
pos2.pow[2]
// res2: Quantity[Refined[Double, Positive], ^[Meter, 2]] = 4.0

// also a multiplicative group
pos2 / pos3
// res3: Quantity[Refined[Double, Positive], /[Meter, Second]] = 0.6666666666666666
pos2.pow[0]
// res4: Quantity[Refined[Double, Positive], 1] = 1.0

The standard refined function for refining values with run-time checking is refineV, which returns an Either. The coulomb-refined package supplies a similar variation refinedVU. These objects are also supported by algebras.

// This refinement succeeds, and returns a Right value
val pe1 = refineVU[Positive, Meter](1)
// pe1: Quantity[Either[String, Refined[Int, Positive]], Meter] = Right(
//   value = 1
// )

// This refinement fails, and returns a Left value
val pe2 = refineVU[Positive, Meter](0)
// pe2: Quantity[Either[String, Refined[Int, Positive]], Meter] = Left(
//   value = "Predicate failed: (0 > 0)."
// )

// positives are an additive semigroup
pe1 + pe1
// res5: Quantity[Either[String, Refined[Int, Positive]], Meter] = Right(
//   value = 2
// )

// algebras operating on Left values result in a Left
pe1 + pe2
// res6: Quantity[Either[String, Refined[Int, Positive]], Meter] = Left(
//   value = "Predicate failed: (0 > 0)."
// )

Policies

policy overlays

The coulomb-refined package currently provides a single "overlay" policy. An overlay policy is designed to work with any other policies currently in scope, and lift them into another abstraction; in this case, lifting policies for value type(s) V into Refined[V, P]. The Refined abstraction guarantees that a value of type V satisfies some predicate P, and the semantics of V remain otherwise unchanged.

For example, given any algebra in scope for a type V that defines addition, the coulomb-refined overlay defines the corresponding Refined[V, P] addition like so:

plus(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] =
// (x.value + y.value) refined by P

Because the refined algebraic policy is an overlay, you can use it with your choice of base policies, for example with core policies or spire policies.

algebraic policy table

The following table summarizes the "algebraic" overlay policy. Examples of Fractional value types include Double, Float, BigDecimal, spire Rational, etc. Integral value types include Int, Long, BigInt, etc.

Value Type Predicate Add Alg Mult Alg + * / pow (exponent)
Fractional Positive semigroup group Y Y Y Y (rational)
Fractional NonNegative semigroup semigroup Y Y N Y (pos int)
Integral Positive semigroup semigroup Y Y N Y (pos int)
Integral NonNegative semigroup semigroup Y Y N Y (pos int)

The table above also applies to Either objects returned by refineVU as discussed in the examples section above.