-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make RefinedTypeOps definition more ergonomic #253
Comments
I 👍 the solution from the opaque type definition is cleaner : more readable and the companion object fulfill more its role of, well, companion. |
Unfortunately the second solution does not seem doable at the moment. Scala is missing inline constructor parameters. Since this feature will probably not land until long time, maybe it's worth to implement the first proposal instead? What do you think @vbergeron-ledger? |
Wouldn't be possible to use a match type for this? opaque type Sats = Long :| GreaterEqual[0] & LessEqual[100000000 * 21000000]
object Sats extends NewType.Derive[Sats] object NewType {
type Derive[A] = A match
case a :| c => RefinedTypeOps[a, c, A]
} |
It was actually the first design for RefinedTypeOps but it does not work correctly with opaque types once you escape their definition scope. |
So here is a list of thing I (re-)tried to make the following example work: opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature] val temp = Temperature(5) //and getting an error when using a negative number Match typeThe most "vanilla" solution: type Derive[T] = T match
case IronType[a, c] => RefinedTypeOps[a, c, T] This works, even for opaque types, until we try to use the newtype in another module. val temp = Temperature(5)
Also tested with this alternative: type ExtractBase[T] = T match
case IronType[a, _] => a
type ExtractConstraint[T] = T match
case IronType[_, c] => c "Mirroring" the opaque typeThe idea is to do something similar to trait RefinedTypeOpsMeta[T]:
type BaseType
type ConstraintType
transparent inline given rtmeta[T]: RefinedTypeOpsMeta[T] = ${rtmetaImpl[T]}
def rtmetaImpl[T : Type](using Quotes): Expr[RefinedTypeOpsMeta[T]] =
import quotes.reflect.*
val ironType = TypeRepr.of[IronType]
val tType = TypeRepr.of[T]
tType.dealias match
case AppliedType(ironType, List(baseType, constraintType)) =>
type Base
type Constr
given Type[Base] = baseType.asType.asInstanceOf[Type[Base]]
given Type[Constr] = constraintType.asType.asInstanceOf[Type[Constr]]
//Instantiation of the `RefinedTypeOpsMeta` (too big for the example)
case t =>
report.errorAndAbort(s"Type does not seems to be an IronType: ${t.show(using Printer.TypeReprStructure)}") With this approach, this work: trait RefinedTypeOps[T]:
val meta: RefinedTypeOpsMeta[T] opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature]:
override val meta: RefinedTypeOpsMeta[Temperature] = rtmeta[Temperature] But it is verbose and feel less natural than the other solutions. The more ergonomic way would be to use something like: trait RefinedTypeOps[T]:
val meta: RefinedTypeOpsMeta[T] = rtmeta[T] or trait RefinedTypeOps[T](using meta: RefinedTypeOpsMeta[T]) but these two options do not work. The first one complains at compile-time that it does not know anything about |
What about the following ?
The usage will be the same, although methods are carried by a val. |
Looks good to me but how about object methods? We could use extensions methods but that would require importing them manually since AFAIK they are searched by the compiler like "normal functions": opaque type Temperature = Double :| Positive
val Temperature = RefinedTypeOps.fromMeta[Temperature](rtmeta)
extension (a: Temperature)
def +(b: Temperature): Temperature = Temperature(a+b) There might also be a problem with If so, I think the following way while being "less intuitive" is probably more ergonomic: object Sats extends RefinedTypeOps[Long, GreaterEqual[0] & LessEqual[100000000 * 21000000]]
opaque type Sats = Sats.Type I'll work on other tasks for 3.0.0 to let the discussion continue a bit. |
This is the only issue remaining before releasing 3.0.0. I still think that in Scala's current state, the 2nd option (see example below) is better. object Sats extends RefinedTypeOps[Long, GreaterEqual[0] & LessEqual[100000000 * 21000000]]
opaque type Sats = Sats.Type I'm going to implement it. |
How about using type members in RefinedTypeOps to make the pattern more readable ? Something like this : object Sats extends RefinedType:
type Base = Long
type Constraint = ??? I would also suggest to remove the Ops from the trait name, as it is more about declaring a refined type rather than adding operations to an existing opaque alias. Finally, something we used in our internal framework before using iron was to call the inner type |
So using object Sats extends RefinedType:
type Base = Long
type Constraint = Foo instead of object Sats extends RefinedType[Long, Foo]
Why not and it stills allows those who prefer making the type alias (like me!) to do it. The only drawback is that it forbids making "transparent" new types but I don't think anyone actually uses this "feature". I also think we should make |
Just realized that this definition: object Sats extends RefinedType:
type Base = Long
type Constraint = Foo is actually (AFAIK) not possible because of the required Without it, the new design looks like this: type Temperature = Temperature.T //Unnecessary if using Temperature.T instead.
object Temperature extends RefinedType[Double, Positive] |
Currently, declaring a new type requires to declare the constraint in the opaque type and in
RefinedTypeOps
's second argument which can be problematic for long constraints. Example derived from Valentin Bergeron and Raphaël Lemaitre's talkAn alias can be used to mitigate the boilerplate:
Ideally, the redundancy should be removed either by passing only the third parameter in
RefinedTypeOps
:or defining the type alias from the companion object (like Neotype):
I think defining the ops from the opaque type (first solution) is more natural. It was not possible before but might be doable easily enough since
TypeRepr#dealias
in Scala 3.4+ also dealiases opaque types. Thus, a mirror-like mechanism can (need to be tested) be used internally:On the other hand, solution 2 is pretty easy to do:
Users' opinions need to be collected in order to make a choice.
The text was updated successfully, but these errors were encountered: