Skip to content

Commit

Permalink
refined integration for scala3 (#392)
Browse files Browse the repository at this point in the history
* positive additive semigroup

* refined value res

* trailing spaces

* refined value conversions

* refined unit conversions

* refined algebraic policy

* unit testing class

* test refined add strict

* more lift

* toValue, toUnit, value, show

* support type dealiasing in typestring

* standard addition test

* pay format troll

* test refined standard addition

* test refined multiply strict

* make refined algs concrete classes

* test divide over positives

* test power over positive refined

* stub docs for coulomb-refined

* refined policy concepts

* start mdoc for coulomb-refined

* refined positive example

* finish draft of coulomb-refined doc

* either rule test

* coulomb.refined.syntax

* test either refined lifting

* test either refined lifting

* explicit conversions refined either

* refined either addition

* refined either multiply

* test overflow and underflow

* refined either divide

* refined either power

* add docs for refineVU and Either

* truncating val conv for either

* truncating/delta unit conversions for either
  • Loading branch information
erikerlandson authored Jan 7, 2023
1 parent f800569 commit 8cf0a7d
Show file tree
Hide file tree
Showing 14 changed files with 1,000 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3')
run: mkdir -p spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target .jvm/target .native/target units/.js/target testkit/.jvm/target project/target
run: mkdir -p spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3')
run: tar cf targets.tar spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target .jvm/target .native/target units/.js/target testkit/.jvm/target project/target
run: tar cf targets.tar spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3')
Expand Down
13 changes: 11 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.settings(commonSettings: _*)
.settings(libraryDependencies += "org.typelevel" %%% "spire" % "0.18.0")

lazy val refined = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("refined"))
.settings(name := "coulomb-refined")
.dependsOn(core % "compile->compile;test->test", units % Test)
.settings(commonSettings: _*)
.settings(libraryDependencies += "eu.timepit" %%% "refined" % "0.10.1")

lazy val testkit = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("testkit"))
Expand All @@ -82,7 +90,8 @@ lazy val all = project
.dependsOn(
core.jvm,
units.jvm,
spire.jvm
spire.jvm,
refined.jvm
) // scala repl only needs JVMPlatform subproj builds
.settings(name := "coulomb-all")
.enablePlugins(NoPublishPlugin) // don't publish
Expand All @@ -103,7 +112,7 @@ lazy val unidocs = project
// http://localhost:4242
lazy val docs = project
.in(file("site"))
.dependsOn(core.jvm, units.jvm, spire.jvm)
.dependsOn(core.jvm, units.jvm, spire.jvm, refined.jvm)
.enablePlugins(TypelevelSitePlugin)

// https://github.com/sbt/sbt-jmh
Expand Down
41 changes: 24 additions & 17 deletions core/src/main/scala/coulomb/infra/meta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -371,24 +371,31 @@ object meta:
case (u, e0) :: tail => (u, e0 * e) :: unifyPow(e, tail)

def typestr(using Quotes)(t: quotes.reflect.TypeRepr): String =
// The policy goal here is that type aliases are never expanded.
typestring(t, false)

def typestring(using
Quotes
)(t: quotes.reflect.TypeRepr, dealias: Boolean): String =
import quotes.reflect.*
def work(tr: TypeRepr): String = tr match
// The policy goal here is that type aliases are never expanded.
case typealias(_) => tr.typeSymbol.name
case unitconst(v) => s"$v"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[*] =>
s"(${work(lhs)} * ${work(rhs)})"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[/] =>
s"(${work(lhs)} / ${work(rhs)})"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[^] =>
s"(${work(lhs)} ^ ${work(rhs)})"
case AppliedType(tc, ta) =>
val tcn = tc.typeSymbol.name
val as = ta.map(work)
if (as.length == 0) tcn
else
tcn + "[" + as.mkString(",") + "]"
case t => t.typeSymbol.name
def work(trp: TypeRepr): String =
val tr = if (dealias) trp.dealias else trp
tr match
case typealias(_) => tr.typeSymbol.name
case unitconst(v) => s"$v"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[*] =>
s"(${work(lhs)} * ${work(rhs)})"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[/] =>
s"(${work(lhs)} / ${work(rhs)})"
case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[^] =>
s"(${work(lhs)} ^ ${work(rhs)})"
case AppliedType(tc, ta) =>
val tcn = tc.typeSymbol.name
val as = ta.map(work)
if (as.length == 0) tcn
else
tcn + "[" + as.mkString(",") + "]"
case t => t.typeSymbol.name
work(t)

object typealias:
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/coulomb/testing/testing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ object types:

private def tsmeta[T](using Type[T], Quotes): Expr[String] =
import quotes.reflect.*
Expr(coulomb.infra.meta.typestr(TypeRepr.of[T]))
Expr(coulomb.infra.meta.typestring(TypeRepr.of[T], true))

private def temeta[T1, T2](using
Type[T1],
Expand Down
163 changes: 163 additions & 0 deletions docs/coulomb-refined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# coulomb-refined

The `coulomb-refined` package defines policies and utilities for integrating the
[refined](https://github.com/fthomas/refined#refined-simple-refinement-types-for-scala)
typelevel libraries with `coulomb`.

## Quick Start

### documentation

You can browse the `coulomb-refined` policies
[here](https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb/policy/overlay/refined.html).

### packages

Include `coulomb-refined` with your Scala project:

```scala
libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@"
libraryDependencies += "com.manyangled" %% "coulomb-refined" % "@VERSION@"
```

### import

To import the standard coulomb policy with the refined overlay:

```scala mdoc
// 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](https://github.com/fthomas/refined/issues/932)
until it is ported forward to Scala 3.

```scala mdoc
// 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.

@:callout(info)
The
[table][algebraic-policy-table]
below summarizes the full list of supported `refined` predicates and associated algebras.
@:@

```scala mdoc
import coulomb.units.si.{*, given}
import coulomb.units.us.{*, given}

val pos1 = 1d.withRP[Positive].withUnit[Meter]
val pos2 = 2d.withRP[Positive].withUnit[Meter]
val pos3 = 3d.withRP[Positive].withUnit[Second]

// positive doubles are an additive semigroup
pos1 + pos2

// also a multiplicative semigroup
pos1 * pos2
pos2.pow[2]

// also a multiplicative group
pos2 / pos3
pos2.pow[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.

```scala mdoc
// This refinement succeeds, and returns a Right value
val pe1 = refineVU[Positive, Meter](1)

// This refinement fails, and returns a Left value
val pe2 = refineVU[Positive, Meter](0)

// positives are an additive semigroup
pe1 + pe1

// algebras operating on Left values result in a Left
pe1 + pe2
```

## 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:
```scala
plus(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] =
// (x.value + y.value) refined by P
```

@:callout(info)
Because the refined algebraic policy is an overlay,
you can use it with your choice of base policies,
for example with
[core policies](concepts.md#coulomb-policies)
or
[spire policies](coulomb-spire.md#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) |

@:callout(info)
The table above also applies to `Either` objects returned by `refineVU` as discussed
in the examples section above.
@:@
1 change: 1 addition & 0 deletions docs/directory.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ laika.navigationOrder = [
develop.md
coulomb-units.md
coulomb-spire.md
coulomb-refined.md
]
114 changes: 114 additions & 0 deletions refined/src/main/scala/coulomb/conversion/standard/unit.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2022 Erik Erlandson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package coulomb.conversion.refined

import scala.util.{Try, Success, Failure}

import coulomb.conversion.*

import eu.timepit.refined.*
import eu.timepit.refined.api.*
import eu.timepit.refined.numeric.*

object unit:
given ctx_UC_Refined_Positive[V, UF, UT](using
uc: UnitConversion[V, UF, UT],
vld: Validate[V, Positive]
): UnitConversion[Refined[V, Positive], UF, UT] =
(v: Refined[V, Positive]) =>
refineV[Positive].unsafeFrom(uc(v.value))

given ctx_UC_Refined_NonNegative[V, UF, UT](using
uc: UnitConversion[V, UF, UT],
vld: Validate[V, NonNegative]
): UnitConversion[Refined[V, NonNegative], UF, UT] =
(v: Refined[V, NonNegative]) =>
refineV[NonNegative].unsafeFrom(uc(v.value))

given ctx_TUC_Refined_Positive[V, UF, UT](using
uc: TruncatingUnitConversion[V, UF, UT],
vld: Validate[V, Positive]
): TruncatingUnitConversion[Refined[V, Positive], UF, UT] =
(v: Refined[V, Positive]) =>
refineV[Positive].unsafeFrom(uc(v.value))

given ctx_TUC_Refined_NonNegative[V, UF, UT](using
uc: TruncatingUnitConversion[V, UF, UT],
vld: Validate[V, NonNegative]
): TruncatingUnitConversion[Refined[V, NonNegative], UF, UT] =
(v: Refined[V, NonNegative]) =>
refineV[NonNegative].unsafeFrom(uc(v.value))

given ctx_DUC_Refined_Positive[V, B, UF, UT](using
uc: DeltaUnitConversion[V, B, UF, UT],
vld: Validate[V, Positive]
): DeltaUnitConversion[Refined[V, Positive], B, UF, UT] =
(v: Refined[V, Positive]) =>
refineV[Positive].unsafeFrom(uc(v.value))

given ctx_DUC_Refined_NonNegative[V, B, UF, UT](using
uc: DeltaUnitConversion[V, B, UF, UT],
vld: Validate[V, NonNegative]
): DeltaUnitConversion[Refined[V, NonNegative], B, UF, UT] =
(v: Refined[V, NonNegative]) =>
refineV[NonNegative].unsafeFrom(uc(v.value))

given ctx_TDUC_Refined_Positive[V, B, UF, UT](using
uc: TruncatingDeltaUnitConversion[V, B, UF, UT],
vld: Validate[V, Positive]
): TruncatingDeltaUnitConversion[Refined[V, Positive], B, UF, UT] =
(v: Refined[V, Positive]) =>
refineV[Positive].unsafeFrom(uc(v.value))

given ctx_TDUC_Refined_NonNegative[V, B, UF, UT](using
uc: TruncatingDeltaUnitConversion[V, B, UF, UT],
vld: Validate[V, NonNegative]
): TruncatingDeltaUnitConversion[Refined[V, NonNegative], B, UF, UT] =
(v: Refined[V, NonNegative]) =>
refineV[NonNegative].unsafeFrom(uc(v.value))

given ctx_UC_Refined_Either[V, P, UF, UT](using
uc: UnitConversion[Refined[V, P], UF, UT]
): UnitConversion[Either[String, Refined[V, P]], UF, UT] =
(v: Either[String, Refined[V, P]]) =>
Try(v.map(uc)) match
case Success(x) => x
case Failure(e) => Left(e.getMessage)

given ctx_TUC_Refined_Either[V, P, UF, UT](using
uc: TruncatingUnitConversion[Refined[V, P], UF, UT]
): TruncatingUnitConversion[Either[String, Refined[V, P]], UF, UT] =
(v: Either[String, Refined[V, P]]) =>
Try(v.map(uc)) match
case Success(x) => x
case Failure(e) => Left(e.getMessage)

given ctx_DUC_Refined_Either[V, B, P, UF, UT](using
uc: DeltaUnitConversion[Refined[V, P], B, UF, UT]
): DeltaUnitConversion[Either[String, Refined[V, P]], B, UF, UT] =
(v: Either[String, Refined[V, P]]) =>
Try(v.map(uc)) match
case Success(x) => x
case Failure(e) => Left(e.getMessage)

given ctx_TDUC_Refined_Either[V, B, P, UF, UT](using
uc: TruncatingDeltaUnitConversion[Refined[V, P], B, UF, UT]
): TruncatingDeltaUnitConversion[Either[String, Refined[V, P]], B, UF, UT] =
(v: Either[String, Refined[V, P]]) =>
Try(v.map(uc)) match
case Success(x) => x
case Failure(e) => Left(e.getMessage)
Loading

0 comments on commit 8cf0a7d

Please sign in to comment.