Open erikerlandson opened 4 years ago
Can you point me to the relevant code?
I think it's worth doing, but perhaps in a different way. I like that singleton-ops gives the "feel" of similar term algebraic behavior. The problem is that if we try to do in types Frac1 + Frac2
it will not work and we need to directly invoke fraction.Add[Frac1, Frac2]
. The reason this doesn't work as-is is because the singleton-ops macro handles all (the known) possible operator variations since we don't have polymorphic calls to 'type operations'.
Can we do better? Can we make things more generic?
I believe so.
We can change it so that when a macro finds an unsupported situation, e.g. +[Frac, Frac]
, it will try to invoke an implicit of that type signature. Since the output of that operation is also a Frac
, the result will be CalcUnknown
. We may need to require a little more additional machinery, but that's no biggy I think.
So essentially, if we do this right, the type classes will be defined for the current op types (other than the new unique operations for Frac). I think new operations should use the same mechanism with new Op Ids.
Let me clarify further.
If we do +[Frac1, Frac2]
and in scope you already have
implicit def fracAdd[Frac1 <: Frac[_,_], Frac2 <: Frac[_,_]] : Frac1 + Frac2 = ???
then this fracAdd
will be called because it's a more specific signature.
However, once you have a composition
Frac1 + Frac2 + Frac3
, then the macro will be called. So for this case we need to change the macro as I have stated above.
Perhaps to avoid implicit recursion we should add an AlternativeOp
typeclass with the same signature as OpMacro
, so the macro with call it instead.
I think we can probably make that work. I've had problems with inferImplicitValue
in the past, although only in some situations:
https://stackoverflow.com/questions/43097790/scala-syntax-tree-returned-from-inferimplicitvalue-is-failing-to-evaluate
If it was 2.13+ only, I'd feel pretty comfortable having implicits that can parse out sub-expressions, but that also is dicey, pre 2.13 (and is the reason coulomb works only on 2.13 plus)
I'm a little hazy on how the internals of the OpMacro code works. Can the new type be detected inside to avoid a CalcUnknown
?
How does this happen now, with existing singleton types? The case of 1 + 2 + 3
seems like it incurs the same basic structural issue, even if the argument types are native singletons.
I think we can probably make that work. I've had problems with
inferImplicitValue
in the past, although only in some situations: https://stackoverflow.com/questions/43097790/scala-syntax-tree-returned-from-inferimplicitvalue-is-failing-to-evaluateIf it was 2.13+ only, I'd feel pretty comfortable having implicits that can parse out sub-expressions, but that also is dicey, pre 2.13 (and is the reason coulomb works only on 2.13 plus)
That's because it uses c.eval
. We don't need it. It's just something like c.typecheck(q"implicitly[AlternativeOp[$OpTpe, $Arg1Tpe, $Arg2Tpe, $Arg3Tpe]]")
I'm a little hazy on how the internals of the OpMacro code works. Can the new type be detected inside to avoid a
CalcUnknown
?
CalcUnknown
should be perfectly fine. It just keeps the calculated type as-is since it's not Int
, Long
, etc... The only thing I'm currently unsure of is what happens when we have something like CalcUnknown + CalcUnknown
.
How does this happen now, with existing singleton types? The case of 1 + 2 + 3 seems like it incurs the same basic structural issue, even if the argument types are native singletons.
The macro does not have upper-bounds. It's just one big case for everything. So it traverses the type tree calculates 1 + 2
, and takes that calculation to continue on with + 3
.
My current guess is that we just need to add the call to implicitly
here:
https://github.com/fthomas/singleton-ops/blob/master/src/main/scala/singleton/ops/impl/GeneralMacros.scala#L901
So in case there is no supported calculation, it tries to invoke implicitly
with AlternativeOp
and if it succeeds, then it reapplies the calculation on it using TypeCalc(alternativeTpe)
.
Thanks @soronpo! I'll play around with it when I get a chance
Wow, this is really cool. If this actually works, singleton-ops will make it very easy to define recursive type operations:
//Needs to be added to singleton-ops
trait AlternativeOp[N, S1, S2, S3] extends OpMacro[N, S1, S2, S3] {
type OutWide = None.type
val valueWide : OutWide = None
val isLiteral : Boolean = false
}
//Fibonacci example
trait FibId
type Fib[P] = AlternativeOp[FibId, P, 0, 0]
implicit def doFib[P](implicit op : ITE[P == 1, 1, ITE[P == 0, 0, Fib[P - 1] + Fib[P - 2]]]) : Fib[P]{type Out = op.Out} = new Fib[P] {
override type Out = op.Out
override val value : op.Out = op.value
}
Libra does this. I've wanted it in coulomb for a while (https://github.com/erikerlandson/coulomb/issues/83). I could just port the code, but it seems like it might be a good fit for abstracting into singleton-ops, if stakeholders are agreeable.