Reputation: 1241
I am trying to get the compiler to infer some type based on 2 other type parameters that are known. here's the example:
trait ReturnCount
trait ReturnsMany extends ReturnCount
trait ReturnsOne extends ReturnCount
class Query[R <: ReturnCount]{
def join[R2 <: ReturnCount,R3 <: ReturnCount](q: Query[R2]): Query[R3]
}
As you can see here I want to be able to join two read queries (the details of composition for which is irrelevant). the resulting new query has to be a query that either ReturnsOne
or ReturnsMany
results. the rule of resolution is also pretty simple: Only if both queries ReturnsOne
then the joined query also ReturnsOne
in all other cases it ReturnsMany
.
So:
val q1 = new Query[ReturnsOne]
val q2 = new Query[ReturnsMany]
val q3 = q1 join q2 //I don't want to have
//to specify R3 because compiler should do it for me...
How can I hope to achieve this?
Upvotes: 3
Views: 141
Reputation: 15086
This is how you would do such a thing for the case where those 3 traits are sealed. If they are open to extension it will probably become more complicated.
It boils down to the following. You have a trait Fancy
with two input types A
and B
, and one output type Out
. You define the implicit instances such that in the case where A
and B
are both ReturnsOne
, Out
will be ReturnsOne
. In all other cases you fall back to a default case with a lower priority (because otherwise you will get ambiguity errors) where Out
always is ReturnsMany
.
scala> :paste
// Entering paste mode (ctrl-D to finish)
sealed trait ReturnCount
sealed trait ReturnsMany extends ReturnCount
sealed trait ReturnsOne extends ReturnCount
sealed trait Fancy[A, B] {
type Out <: ReturnCount
}
object Fancy extends LowerPriority {
type Aux[A,B,Out1 <: ReturnCount] = Fancy[A,B] { type Out = Out1 }
implicit def returnsOne: Fancy.Aux[ReturnsOne,ReturnsOne,ReturnsOne] =
new Fancy[ReturnsOne,ReturnsOne] { type Out = ReturnsOne }
}
trait LowerPriority {
implicit def returnsMany[A,B]: Fancy.Aux[A,B,ReturnsMany] =
new Fancy[A,B] { type Out = ReturnsMany }
}
class Query[R <: ReturnCount] {
def join[R2 <: ReturnCount](q: Query[R2])(implicit fancy: Fancy[R,R2]): Query[fancy.Out] = ???
}
// Exiting paste mode, now interpreting.
defined trait ReturnCount
defined trait ReturnsMany
defined trait ReturnsOne
defined trait Fancy
defined object Fancy
defined trait LowerPriority
defined class Query
scala> :type new Query[ReturnsOne].join(new Query[ReturnsOne])
Query[ReturnsOne]
scala> :type new Query[ReturnsOne].join(new Query[ReturnsMany])
Query[ReturnsMany]
scala> :type new Query[ReturnsMany].join(new Query[ReturnsMany])
Query[ReturnsMany]
scala> :type new Query[ReturnsMany].join(new Query[ReturnsOne])
Query[ReturnsMany]
In a real scenario your Fancy
will probably also need a join
method which will contain the actual implementations that your Query#join
method delegates to:
class Query[R <: ReturnCount] {
def join[R2 <: ReturnCount](q: Query[R2])(implicit fancy: Fancy[R,R2]): Query[fancy.Out] =
fancy.join(this, q)
}
This blog might be a good starting point to learn more about such patterns.
Upvotes: 3