GithubHelp home page GithubHelp logo

iltotore / iron Goto Github PK

View Code? Open in Web Editor NEW
429.0 14.0 38.0 1.37 MB

Strong type constraints for Scala

Home Page: https://iltotore.github.io/iron/docs/

License: Apache License 2.0

Scala 95.41% Shell 2.68% Batchfile 1.91%
types assert scala functional-programming refinement-types

iron's Introduction

logo

Scala version support CI


Iron is a lightweight library for refined types in Scala 3.

It enables attaching constraints/assertions to types, to enforce properties and forbid invalid values.

  • Catch bugs. In the spirit of static typing, use more specific types to avoid invalid values.
  • Compile-time and runtime. Evaluate constraints at compile time, or explicitly check them at runtime (e.g. for a form).
  • Seamless. Iron types are subtypes of their unrefined versions, meaning you can easily add or remove them.
  • No black magic. Use Scala 3's powerful inline, types and restricted macros for consistent behaviour and rules. No unexpected behaviour.
  • Extendable. Easily create your own constraints or integrations using classic typeclasses.

To learn more about Iron, see the microsite.

Example

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.*

def log(x: Double :| Positive): Double =
  Math.log(x) //Used like a normal `Double`

log(1.0) //Automatically verified at compile time.
log(-1.0) //Compile-time error: Should be strictly positive

val runtimeValue: Double = ???
log(runtimeValue.refineUnsafe) //Explicitly refine your external values at runtime.

runtimeValue.refineEither.map(log) //Use monadic style for functional validation
runtimeValue.refineEither[Positive].map(log) //More explicitly

Helpful error messages

Iron provides useful errors when a constraint does not pass:

log(-1.0)
-- Constraint Error --------------------------------------------------------
Could not satisfy a constraint for type scala.Double.

Value: -1.0
Message: Should be strictly positive
----------------------------------------------------------------------------

Or when it cannot be verified:

val runtimeValue: Double = ???
log(runtimeValue)
-- Constraint Error --------------------------------------------------------
Cannot refine non full inlined input at compile-time.
To test a constraint at runtime, use the `refine` extension method.

Note: Due to a Scala limitation, already-refined types cannot be tested at compile-time (unless proven by an `Implication`).

Inlined input: runtimeValue
----------------------------------------------------------------------------

Import in your project

SBT:

libraryDependencies += "io.github.iltotore" %% "iron" % "version"

Mill:

ivy"io.github.iltotore::iron:version"

Note: replace version with the version of Iron.

Platform support

Module JVM JS Native
iron ✔️ ✔️ ✔️
iron-borer ✔️ ✔️
iron-cats ✔️ ✔️ ✔️
iron-circe ✔️ ✔️ ✔️
iron-ciris ✔️ ✔️ ✔️
iron-decline ✔️ ✔️ ✔️
iron-doobie ✔️
iron-jsoniter ✔️ ✔️ ✔️
iron-scalacheck ✔️ ✔️
iron-skunk ✔️ ✔️ ✔️
iron-upickle ✔️ ✔️ ✔️
iron-zio ✔️ ✔️
iron-zio-json ✔️ ✔️

Adopters

Here is a non-exhaustive list of projects using Iron.

Submit a PR to add your project or company to the list.

Companies

Other projects

Useful links

iron's People

Contributors

ajaychandran avatar antonkw avatar dependabot[bot] avatar gvolpe avatar iltotore avatar jnicoulaud-ledger avatar jprudent avatar kyri-petrou avatar masynchin avatar matwojcik avatar mcallisto avatar nox213 avatar rayandfz avatar rlemaitre avatar rparree avatar sethtisue avatar sirthias avatar vbergeron avatar vbergeron-ledger avatar xuwei-k avatar zaxxel avatar zelenya avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

iron's Issues

Wrap scala.compiletime.ops operators using match types

Scala 3 has many operator types to manipulate and compare value types. There are useful especially for Iron 2 implications:

//Example from io.github.iltotore.iron.constraint.numeric (Greater constraint transitivity)
inline given[V1, V2](using V1 > V2 =:= true): (Greater[V1] ==> Greater[V2]) = Implication()

However, similar operators for Double, Int, String etc... are not the same:

import scala.compiletime.constValue
import scala.compiletime.ops.*

constValue[int.+[1, 1]] //2: Int
constValue[double.+[1.0, 1.0]] //2.0: Double
constValue[double.+[1, 1]] //Error
constValue[double.+[int.ToDouble[1], int.ToDouble[1]] //2.0: Double

We can abstract away these low-level operators by using conversions like int.ToDouble in a match type:

type +[A, B] = (A, B) match
  case (Int, Int) => int.+[A, B]
  case (Int, Double) => double.+[int.ToDouble[A], B]
  case (Double, Int) => double.+[A, int.ToDouble[B]]
  case (Double, Double) => double.+[A, B]
  case (String, ?) => string.+[A, ToString[B]]
  case (?, String) => string.+[ToString[A], B]
constValue[1 + 1] //2: Int
constValue[1.0 + 1.0] //2.0: Double
constValue[1 + 1.0] //2.0: Double
constValue["Should be greater than " + 1] //"Should be greater than 1": String

These abstractions should be added in io.github.iltotore.iron.ops to ease type operations.

Support for `jsoniter-scala` codecs

Is your feature request related to a problem? Please describe.
jsoniter-scala is the fastest json parser available in Scala. It's becoming quite popular, and several libraries already provide support for it (e.g., tapir, sttp, caliban (WIP)).

It would be good if Iron provided support for it same way that it does for circe / zioJson.

The implementation for jsoniter-scala is slightly less straightforward than circe / zioJson since it makes heavy use of macros for derivation and doesn't provide implicits for standard types. This could easily dissuade a user from using Iron if they can't figure the interop out easily / quickly.

Describe the solution you'd like
Very similar to the circe / zioJson approach, provide a given / method with which the user can create codecs for jsoniter-scala.

If you think that this is something that you would like to add to the library, I can make a PR for it (I already have a working example - just let me know and I'll open a PR)

Support full inlining from composed constraints

Currently, constraints like Not, Or and And don't support compile-time evaluation due to a Dotty limitation with inline class parameters.

The considered solution is to use macros for this kind of constraint.

Notes:

  • This is an experimental draft. Its implementation is not assured.
  • This issue is temporarly added to milestone 1.1.2. It will eventually be moved to 1.2.0 if it requires breaking changes.

Add an "assumed constraint" method

Say we have something like this:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.GreaterEqual

val rdm: Int = util.Random.nextInt(10)
val x: Int :| GreaterEqual[0] = rdm

This does not compile because rdm is a runtime value. The usual way to handle such situation is to use a runtime refinement method like rdm.refine but since we know that util.Random.nextInt(10) is always >= 0, we don't need the extra if provided by refine.

Therefore, refine is often replaced in such case by asInstanceOf which works like a charm in this example. However, this is prone to careless errors. Here is a mistake I made while making the ScalaCheck module:

def intervalArbitrary[A : Numeric : Choose, C](min: A, max: A): Arbitrary[A :| C] =
    Gen.chooseNum(min, max).asInstanceOf //I forgot Arbitrary.apply!

I only knew about my error at runtime (ClassCastException).

A simple yet useful solution would be to add a new extension method like A.assume[C] (or a better name) which would only cast the constraint and not the value itself:

val rdm: Int = util.Random.nextInt(10)
val x: Int :| GreaterEqual[0] = rdm.assume //OK
def intervalArbitrary[A : Numeric : Choose, C](min: A, max: A): Arbitrary[A :| C] =
    Gen.chooseNum(min, max).assume //Type mismatch at compile-time!
def intervalArbitrary[A : Numeric : Choose, C](min: A, max: A): Arbitrary[A :| C] =
    Arbitrary(Gen.chooseNum(min, max)).assume //OK

This might be a simple first issue for anyone whishing to contribute to Iron. You should create a this method in main/src/io/github/iltotore/iron/package.scala taking inspiration from refine.

Add String concactenation support in messages

Describe the solution you'd like
Add compile-time String concactenation support in messages. Currently, constraits's message with concactenated strings can only be evaluated at runtime (gives the <Unknown> message at compile-time instead).

Describe alternatives you've considered
A potential solution would be to create our own instance of FromExpr[String] based on the stdlib's one with this new case:

  • Concactenated values can be evaluated at compile-time if all values can be recovered using another FromExpr[T]

Avoid redundant evaluations using `Constrained` generic types

Is your feature request related to a problem? Please describe.
In its current state, Iron doesn't re-evaluate the constraint in this case:

type Cosinus = Double / (-1d <= ?? <= 1d)

def cos(x: Double): Cosinus = ???
def acos(x: Cosinus): Refined[Double] = ???

//Runtime
val x = cos(runtimeValue)
val y = acos(x) //No verification of `x` constraint since the passed value is a `Constrained[Double, -1d <= ?? <= 1d]`

But will do in this case:

def f(x: Double / (Cosinus && AnotherConstraint)): Refined[Double] = ???

val x = cos(runtimeValue)
val y = f(x) //AnotherConstraint AND Cosinus are (re)-evaluated but x is already a Cosinus

Same problem with "consequence-less" constraints like DescribedAs

Describe the solution you'd like

  • Iron should use Scala's typing system to determine which constraint needs to be tested
  • Consequences of some special constraints should be taken into account. For example, a value of Double < 0d should not be tested when converted to Double < 1d

Describe alternatives you've considered
The currently planned solution is to use implicit conversions.
To avoid spaghettis and headhaches, these conversions will be created internally. The user should handle itself as little as possible and use the API instead.

Add ZIO/ZIO Prelude support

Describe the solution you'd like
Iron has a support module for Cats which add convenience methods for accumulative error handling and typeclass instances.

A iron-zio could be created for the exact same purpose but for the ZIO Ecosystem, adding instances for ZIO's Associative, Commutative etc... and adding refineNel/refineNec equivalents for ZIO Prelude's Validation.

Add shapeless module

Add shapeless-oriented features

Globally, Iron is already compatible with Shapeless but the module can provide some implementation-level features like:

  • Compile-time Size/MinSize/MaxSize/etc... constraint evaluation on HList.
  • Other compile-time optimizations using Shapeless and the type system.

Move to Scala 3.2.+

Scala 3.2.+ contains some useful features for Iron including the removal of the "opaque types + inline" implementation restriction.

Since 3.2.+ is not binary compatible, this is considered as a breaking change.

Cleanup code and refactor project

Iron heavily changed since 1.0.0 and some places weren't refactored to preserve retrocompatibility.

Since 2.0.0 is the next major release, this is the good moment to refactor and cleanup the entire codebase.

Scalacheck support

Is your feature request related to a problem? Please describe.
I would like to use the library with scalacheck

Describe the solution you'd like
Introduce a module to derive Arbitrary instances for use with Scalacheck.

I realize this might be difficult for e.g. regexes, but I would be happy with something that works for lengths and bounds, since regex constraints often imply some pretty specific generators that the user should define.

Embed iron-numeric, string and collection into the main module

Embed these "primitive" modules into the main one due to their lightness.

Envisaged structure:

+ <main package>
  + constraint
      + any: base constraints (e.g Not, Or...) and constraints working on any type e.g (StrictEqual)
      + numeric
      + ...

Note that other modules like iron-circe or iron-cats will remaing external.

6

Describe the bug
Scala environment: JVM/Scala.JS/ScalaNative
Scala version:
Iron version:

Reproduction steps
The steps to reproduce the described issue

  • Step 1
  • Step 2

Expected behavior
A clear and concise description of what you expected to happen.

Current behavior
A clear and concise description of what actually happens

Helpful implicit resolution messages

The union (and presumably, intersection) failed implicit search message are not very useful :
(see #110 for more context)
Could not find implicit AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class iltotore)),object iron),Constraint),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Long), AppliedType(TypeRef(TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class iron)),object constraint),any),StrictEqual),List(ConstantType(Constant(0))))))

Ideally a more descriptive message, including the list of all failed constraints, should be displayed.

Consider using union/intersection types instead of `&&` and `||`

Scala 3 has union and intersection types.

However, Iron 1 (and current dev of Iron 2) uses custom type aliases || and && due to several union/intersection limitations of union/intersection types, including problems with union commutativity.

This project found a way to derive typeclasses with unions the expected way. The ability to use Scala vanilla's union/intersection types would bring several benefits including:

  • Commutativity: Int :| (Greater[0] | StrictEqual[0]) is automatically the same type than Int :| (StrictEqual[0] | Greater[0])
  • Coherence: the union/intersection types feel more idiomatic than || and &&
  • It does not shadow existing || and && defined in scala.compiletime.ops.boolean

Placeholder algebra

Allow placeholder for constraints:

def f(x: Double ==> (-1 < ? < 1)): Double = ???

desugars to:

def f(x: Double ==> (Greater[-1] && Lower[1])): Double = ???

Note: If you're not in future-strict mode you can use _ for lambda-like placeholder:

def f(x: Double ==> (-1 < _ < 1)): Double = ???

Use monad for runtime constraints

Feature

Iron should use a monad like an Either` instead of throwing an exception when parsing.

Draft example:

inline def log(x: Double > 0d): Either[IllegalValueError, Double] = x.map(Math.log)

Motivations

This allow better integration with monads which are an important part of Scala and Functional Programming. This also allows functional bad values handling.

Example:

val x: Double = ???

val processed: Either[IllegalValueError, Double] = for {
  a <- log(x) //Can fail. Returns an Either[IllegalValueError, Double] (Right if successful, Left otherwise).
  b <- foo(a) //Some post process
} yield b

Bring BinaryOperator and other mathematical constraints closer to mathematical relations

Describe the solution you'd like
Following the same philosophy as v1.1.2, the goal is to introduce the same relation system as in mathematics. This issue is potentially linked to #38 since some relations can be inferred using properties of some relations:

val x: Double < 0d = -5
val y: Double < 1d = x //By transitivity, if x < 0 and 0 < 1, then x < 1d: no test required.

Describe alternatives you've considered
The investigated solution is an alias for basic relational properties including:

  • Transitivity
  • Reflexivity
  • Symmetry
  • Antisymmetry
  • etc...

And aliases for some "complex" relations:

  • Equivalence (Transitivity + Reflexivity + Symmetry)
  • Ordering (Transitivity + Reflexivity + Antisymmetry)
  • etc...

Add RuntimeOnly[B] modifier

A recent discussion (#27) showed the usefulness of a RuntimeOnly modifier for constraints.

This constraint should be added in the standard constraints.

Automatic conversion from uncostrained type no longer works in Scala 3.1.1

Describe the bug
Scala environment: JVM
Scala version: 3.1.1
Iron version: 1.2.0

Reproduction steps

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.*
import io.github.iltotore.iron.numeric.constraint.*

def mult(x: Int > 0, y: Int > 0): Either[IllegalValueError[?], Int] =
  for   
    a <- x
    b <- y
  yield a * b

def pow(z: Int): Either[IllegalValueError[?], Int] = mult(z, z)

pow(5)

The same is available as a Scastie here

Expected behavior
Up to Scala version 3.1.0 included
Right(25): scala.util.Either[io.github.iltotore.iron.constraint.IllegalValueError[_ >: scala.Nothing <: scala.Any], scala.Int]

Current behavior
With Scala version 3.1.1
the pow method fails to compile, with the following error:

cannot reduce inline match with
 scrutinee:  0 : (0 : Int)
 patterns :  case b @ _:Int if a.>(b)
             case b @ _:Int if a.==(b)
             case b @ _:Int if a.<(b)
             case b @ _:Int

Add IO module

Iron should provide some IO-related constraints like:

  • URL/Path specs
  • Common File assertions like Directory, Exist etc...

StackOverflow when refining long string at compile time

Describe the bug

Scala environment: JVM
Scala version: 3.2.2
Iron version: 2.1.0-RC1

Reproduction steps

Declaring any long string value and try it to refine it at compile time. E.g.

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*

type NonBlank = Not[Blank DescribedAs "Textual value must not be blank"]
type NonBlankText = String :| NonBlank

val privateKey: NonBlankText =
  "1SUT8Dv40Ay78zl26xJJrpkP1cDPsLk1V4eVoRQuGceS7hIo8gmKxbAVWdn15lPYQ1fcifRaTINXg6tDH4WNhxBLbgF+Yqd3ouP7kS978LlFUZq3cAC8UAekvIcnJwzxaqsHBG7+mC1Adk3TqlJt+5JELo5r8apwChkxRzeFaybcRD6xUmb8GGJj/FTpatATVAMh1SrkQsaJhucByHxxt9EVUT2GaITsebwdyTRxtGnam5m+ZrspaAkvXCwXgvfVwZEN5wbvwU5dcZ5279KCPZY/HnW9s8SlS8qbTst5z248l0cALe+hyNkNC1Y05u3uEUlhsJ1AD0MLCjBlroHNjvWrWRDoz93qEbakuPb9/Rr0Z6l09V6exmJ2PXGnSUtV1hYo2DAQbOklQZdEZBkhsY4wirsCTKhOainF5UvtLljHDtap9UaMXtoT6g/gazq4"

Here's a Scastie: https://scastie.scala-lang.org/7CsWYwxzSmu7b8XmjP0ENg

Expected behavior
It should compile.

Current behavior
It results in a StackOverflowError.

Strange implicit error using opaque types

Describe the bug
Scala environment: All
Scala version: 3.2.2
Iron version: 2.0.0

Reproduction steps
Given this setup (so encoding of newtypes using a wraper trait overrided by an opaque type) :

  trait Wrap:
    type T

  trait Newtype[A] extends Wrap:
    opaque type T = A

    inline def apply(in: A): T = in

    extension (value: this.T) //
      inline def unwrap: A = value

  case object Money extends Newtype[Long :| GreaterEqual[0]]

  val foo = Money(123)

Expected behavior
Everything compile fine and we got strong, 0 cost type safety.

Current behavior
Compilation fails with a strange implicit error :

Could not find implicit AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class iltotore)),object iron),Constraint),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Long), AppliedType(TypeRef(TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class iron)),object constraint),any),StrictEqual),List(ConstantType(Constant(0))))))

The inlines does not appear to chane the result. I am investigating bisecting more the error cause.

Create some examples

Add some motivating examples (e.g a form/REST API) projects to show the benefits of Iron.

Add cats module

Add cats module to support some cats features like Parallel with Constrained[A, B].

Regression with autoRefine and autoCastIron

Raising the issue for awareness. This regression was introduced in #70 , I can take a look this evening after work.

[error] -- [E007] Type Mismatch Error: /home/gvolpe/workspace/trading/modules/x-demo/src/main/scala/demo/IronDemo.scala:44:20
[error] 44 |  val alien = Alien("Bob", 120)
[error]    |                    ^^^^^
[error]    |Found:    ("Bob" : String)
[error]    |Required: io.github.iltotore.iron.IronType[String, demo.NameR]
[error]    |Note that implicit conversions cannot be applied because they are ambiguous;
[error]    |both method autoRefine in package io.github.iltotore.iron and method autoCastIron in package io.github.iltotore.iron convert from ("Bob" : String) to io.github.iltotore.iron.IronType[String, demo.NameR]

Better evaluation time support

Because some constraints should be runtime-only or compile time only, Iron needs to provide a better way to handle these cases:

  • Add CompileTimeOnly and RuntimeOnly
  • Add the default fallback option. Allows any constraint to be evaluated at runtime except CompileTimeOnly
  • Iron doesn't try to pre-evaluate RuntimeOnly constraints

Add modules basic modules

Currently, Iron only have a numeric module. In order to allow users to get started more easily, Iron should provide modules for basic data like String/Char and special support or/and examples for popular functional libraries like Cats or ZIO.

Include common constraints in the core library (main module)

Is your feature request related to a problem? Please describe.
When discussing with other users, I realized that Iron's "standard library" lacks of several useful and much-used constraints and/or aliases like PosInt for Greater[0], Char constraints or Blank (or NotBlank) as proposed in #79.

Describe the solution you'd like
I think we should add more of these common-enough constraints to make Iron more useful out of the box/battery included. A good starting point is fthomas/refined's included predicates.

Note: for license reasons, no code should be directly pulled from Refined. The link above is only here to see what are the most common/useful constraints.

Some guidelines:

  • Avoid double-negation (i.e create a Blank constraint instead of a NotBlank and compose with the Not constraint instead)
  • Try to make your constraint usable at both runtime and compile-time if possible
  • If your constraint requires a macro, please put it in the companion object as private
  • Add related Implications if needed
  • Document your constraint
  • Add unit tests for your constraints
  • Submit your changes 🎉

Rework Constraint documentation

The Constraint reference page mixes low-level constraint creation and constraint usage. This tends to not be beginer friendly.

It should be split into two pages:

  • Usage: Attaching a constraint to a type, composing them with union, intersection, DescribedAs... Should not overlap Refinement methods
  • Creation: Creating a new constraint "from scratch" with a Dummy type and Typeclass instance. Should probably be the new "Constraint reference"

Rewrite external modules to Iron 2

Rewrite external modules for Iron 2, including:

  • iron-cats (validation)
  • iron-circe (json encoder/decoder for refined types)
  • Create iron-zio (json encoder/decoder for refined types)

Note: these 3 modules are mainly about typeclasses.

See Iron 1 for module creation/inspiration.

Use SemVer conventions

Use the following conventions for Iron:

  • Use SemVer spec
  • To ease the release and adoption processes, all submodules will have the same version as the main module

Scala.js support

Hi,

First of all, thanks a lot for all the work you've put into this library! I really like it so far 🤩

I decided to feature it in my upcoming book, you can see the corresponding Pull Request.

The Trading application where it's being used cross-compiles its domain module to JS/JVM, so I was wondering if Scala.js support was planned at some point? For now, I got away by having two different implementations of the Symbol datatype, one for each platform.


A few other things that would be awesome to add would be Cats' typeclass instances support. I added the ones I needed in my PR, but I guess it's so common, this could land in the Cats module (happy to contribute the initial ones!).

import cats.*
import io.github.iltotore.iron.*

object IronCatsInstances:
  inline given [A, B](using inline ev: Eq[A]): Eq[A :| B] = ev.asInstanceOf[Eq[A :| B]]
  inline given [A, B](using inline ev: Order[A]): Order[A :| B] = ev.asInstanceOf[Order[A :| B]]
  inline given [A, B](using inline ev: Show[A]): Show[A :| B] = ev.asInstanceOf[Show[A :| B]]
  • Rename modules' package
  • Add Cats' typeclass instances
  • Add ScalaJS and ScalaNative support

Allow strict compile-time refinements

Is your feature request related to a problem? Please describe.
Constrained-to-constrained evaluation is done at runtime. But people might want to ensure that the passed Constrained meets the requirements at compile-time. For example:

def log(x: Double > 0d): Raw[Double] = refined {
  Math.log(x)
}

def f(x: Double \ 5d): Raw[Double] = refined {
  log(x) / (x - 5) //Positivity of `x` isn't proved at compile-time
}

Describe the solution you'd like
The proposed solution is to use create a CompileTimeOnly[B] constraint similar to already existing RuntimeOnly[B]:

def log(x: Double / CompileTimeOnly[Greater[0d]]): Raw[Double] = refined {
  Math.log(x)
}

def f(x: Double \ 5d): Raw[Double] = refined {
  log(x) / (x - 5) //Error: Unable to evaluate CompileTimeOnly assertion
}

def g(x: Double / (Not[5d] && Greater[0d])): Raw[Double] = refined {
  log(x) / (x - 5) //No error
}

This issue is related to #39.

Cannot refine non full inlined input at compile-time

Describe the bug
Scala environment: JVM
Scala version: 3.2.2
Iron version: 2.0.0

Reproduction steps

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.string.*

def test[F[_]: Concurrent](str: String): F[Result] =
  for {
    id <- Concurrent[F].fromEither(Either.catchNonFatal(str.refine[Alphanumeric]))
    result <- doSomething(id)
  } yield result

Expected behavior
This should compile.

Current behavior

Throws a compile-time error:

[error] 215 |            result <- doSomething(id)
[error]     |                                  ^^
[error]     |Cannot refine non full inlined input at compile-time.
[error]     |To test a constraint at runtime, use the `refined` extension method.
[error]     |
[error]     |Note: Due to a Scala limitation, already-refined types cannot be tested at compile-time (unless proven by an `Implication`).
[error]     |
[error]     |Inlined input: id
[error]     |---------------------------------------------------------------------------
[error]     |Inline stack trace
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from Postgres.scala:215
[error]      ---------------------------------------------------------------------------

Also, I couldn't find method inlined (with d at the end)

Add constraint operations

Iron should provide basic "boolean" operations for constraints:

  • And[B, C]
  • Or[B, C]
  • Not[B]

Examples:

//And
type Between[V1, V2] = Greater[V1] && Lower[V2]

//Or
def login(userOrMail: String ==> EmailLike || PseudoLike): Unit = ???

//Inverse
def inverse(x: Double && Not[Equal[0]]): Double = 1/x

Publish snapshot on merge to main branch

sbt-ci-release makes this super easy. I assume mill-ci-release does the same?

This is great for those of us interested in trying out features not yet releases without resorting to a local publish (can't be shared).

Improve bad value system

Improve Iron's illegal value system by applying these changes:

  • Constraints should directly return a Option[String]. Thanks to FromExpr[Option] and FromExpr[String], compile time evaluation is still possible. We can't return diretly an Either or a Refined because they require a FromExpr[A].
  • Use a message field for IllegalValueError to add further information about assertion failure.
  • Drop unused (and not-that-usable) constraint field of IllegalValueError.

More mathematical alias for Constrained[A, B]

The Constrained[A, B] alias (currently ==>) should be changed to / to be more similar to the mathematical equivalent: ∀x € E / P(x).

Example:

def f(x: Double / (Greater[-1] && Greater[1])): Refined[Double] = ???

Note: the old alias ==> will be kept for retro-compatibility reasons until the next major release.

Improve UX of instances in scope

Discussed in #67

Originally posted by gvolpe December 3, 2022
I've prepared the following standalone example to showcase in my upcoming book, and I was wondering if we could do better regarding the import of many given instances.

package demo

import trading.domain.IronCatsInstances.given // this will be import io.github.iltotore.iron.cats.* soon

import cats.*
import cats.derived.*
import io.circe.*
import io.circe.syntax.*
import io.github.iltotore.iron.{ given, * }
import io.github.iltotore.iron.circeSupport.given
import io.github.iltotore.iron.constraint.any.DescribedAs
import io.github.iltotore.iron.constraint.numeric.given
import io.github.iltotore.iron.constraint.numeric.{ Greater, Less }
import io.github.iltotore.iron.constraint.string.given
import io.github.iltotore.iron.constraint.string.{ Alphanumeric, MaxLength, MinLength }

type AgeR = DescribedAs[
  Greater[0] & Less[151],
  "Alien's age must be an integer between 1 and 150"
]

type NameR = DescribedAs[
  Alphanumeric & MinLength[1] & MaxLength[50],
  "Alien's name must be an alphanumeric of max length 50"
]

// format: off
case class Alien(
    name: String :| NameR,
    age: Int :| AgeR
) derives Codec.AsObject, Eq, Show
// format: on

@main def ironDemo =
  val alien = Alien("Bob", 120)
  println(alien.asJson.noSpaces)

Without the explicit given imports, this code would not compile.

Ideally, no explicit given should be visible. This is doable by having instances declared in companion objects directly, you can look at how Cats does it, for example.

If this sounds too complicated, I think we can get away with a single given import.

import io.github.iltotore.iron.{ given, * }

This could re-export the instances of numeric, collection, string and any to give a better user experience.

What do you think?


PS. I'd be happy to have a go at this if you agree with either solution (I'd prefer the former, no givens 😃)

Add companion ops for refined "new types"

Refined has a trait called RefinedTypeOps which enables users to create their own zero-additional-cost types on top of refined types:

type Age = Int Refined Positive
object Age extends RefinedTypeOps[Age, Int]

val age: Age = Age(18)
val invalidAge: Age = Age(-15) //Compile-time error

For now, Iron does not provide such elegant mechanism for "refined new types" and people combine it with libraries like Sprout.

Iron could provide something similar:

type Age = Int :| Positive
object Age extends RefinedTypeOps[Int, Age] //Or better: RefinedTypeOps[Age]

It would synergize greatly with opaque types:

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Temperature]

opaque type Moisture = Double :| Positive
object Moisture extends RefinedTypeOps[Double, Moisture]
val temperature = Temperature(25)
val moisture = Moisture(4)

val invalidTemperature: Temperature = Moisture(5) //Error at compile-time because Moisture not <:< Temperature, even if they're both Double :| Positive

Finally, extension methods would allow libraries to attach custom methods to the companion:

val tempEither: Either[String, Temperature] = Temperature.either(15)
val tempValidation: Validation[String, Temperature] = Temperature.validation(15) //From ZIO Module
// etc...

Allow monad-less error handling

Since 1.0, Iron jumped from raw value/error throwing model to monadic validation (Either, Validated...). This change allowed easy typelevel, validation pipeline but the old method also have pros over Either (example: The mathematical domain).

Iron should use zero-overhead error throwing AND allow monadic error management if needed.

Here is the proposed (not definitive) syntax:

//Raw value

def log(x: Double > 0d): Double = Math.log(x) //Raw value. An error is thrown if x <= 0

def log(x: Double > 0d): Double = { //x is lazy. No error if invalid.
  x //No error if invalid because no implicit conversion to Double is needed
  Math.log(x) //Error if invalid. An implicit conversion is tried
}


//Monadic

def log(x: Double > 0d): RefinedE[Double] = x.toEither.map(Math.log) //Monadic value validation

def log(x: Double > 0d): RefinedNel[Double] = x.toValidatedNel.map(Math.log) //Using Cats validated. Alias for `x.toEither.toValidatedNel`

Thanks to Scala 3.0.1, we can inline the entire validation process for raw values, leading to a zero-overhead, safe, refinement system.

Suggestion: support incremental refine/refineFurther with explicit parameter

Right now it is not possible to incrementally and explicitly refine, as far as I can tell.
This is something I find needing all the time (i want to append more constraints as data travels through my application, without discarding old constraints, or having to re-type them).

I've never written any real scala3 inlines, macros or such before, so I'm sure this is not the most efficient way of solving it, but I found the following useful - maybe it can give you some ideas on how it could be done in an elegant/proper way?:

  extension [Src](value: Src)
    inline def refineZIO[C](using inline constraint: Constraint[Src, C]): IO[String, Src :| C] =
      value.refineValidation[C].toZIO

  extension [Src, Cstr](value: Src :| Cstr)
    inline def refineFurtherZIO[C](using inline constraint: Constraint[Src, C]): IO[String, Src :| (Cstr & C)] =
      (value: Src).refineZIO[C].map(_.asInstanceOf[Src :| (Cstr & C)])
    inline def refineFurtherEither[C](using inline constraint: Constraint[Src, C]): Either[String, Src :| (Cstr & C)] =
      (value: Src).refineEither[C].map(_.asInstanceOf[Src :| (Cstr & C)])
    inline def refineFurtherOption[C](using inline constraint: Constraint[Src, C]): Option[Src :| (Cstr & C)] =
      (value: Src).refineOption[C].map(_.asInstanceOf[Src :| (Cstr & C)])
    inline def refineFurther[C](using inline constraint: Constraint[Src, C]): Src :| (Cstr & C) =
      (value: Src).refine[C].asInstanceOf[Src :| (Cstr & C)]

This allows me to further refine already refined types, with a simple syntax (replace my naming and impl with something you prefer!) and append more and more refinements as I go, without discarding the original one

example

for {
  x1           <- 0.refineZIO[Less[1] // Int :| Less[1]
  _            <- foo1(x1)
  _            <- bar1(x1)

/// data travels further through the application, many method calls, storage, read from storage, etc
  x2           <- x1.refineFurtherZIO[Less[2]] // Int :| (Less[1] & Less[2])
  _            <- foo2(x2)
  _            <- bar2(x2)

/// data travels further through the application, many method calls, storage, read from storage, etc
  x3           <- x2.refineFurtherZIO[Less[3]] // Int :| (Less[1] & Less[2] & Less[3])
  _            <- foo3(x3)
  _            <- bar3(x3)
} yield ()

If there is an existing way to append constraints like this that I'm not seeing, please let me know

`URLLike` constraint fails on `%` characters

Describe the bug
Scala environment: JVM
Scala version: 3.2.1
Iron version: 2.0.0-RC1-19-cb7d80-DIRTY12775d8a-SNAPSHOT (latest snapshot)

The % is a valid URL character, which is used when encoding strings into URLs. For example, %20 can used to represent a space

Reproduction steps

val url: String |: URLLike = "http://example.com/?q=with%20space" // Doesn't compile
val url: String |: URLLike = "http://example.com/?q=with+space" // compiles

Expected behavior
Should compile since it's a valid URL

Current behavior
Doesn't compile

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.