I’ve just released Smockito, a tiny Scala 3 facade for Mockito. One of the things I strived for was to achieve better type safety than Mockito with a simple API. Take a look at this declaration:
opaque type MockedMethod[A <: Tuple, R] = A => R // implicit conversion provided
inline def on[A1 <: Tuple, A2 <: Tuple, R1: ClassTag, R2: ClassTag]
(method: Mock[T] ?=> MockedMethod[A1, R1])
(using A1 =:= A2, R1 =:= R2, ValueOf[Size[A1]])
(stub: PartialFunction[A2, R2]): Mock[T]
This allows mocking as in:
mock.on(it.aMethod)(args => ...)
Works great, and type checks well. However, if I move the A1 =:= A2 and R1 =:= R2 constraints to either the first or last parameter list positions (instead of the middle), it no longer manages to prove the equality of the types and/or allows the widening to Any, likely due to the variance of the Scala function types. Why is the position relevant?
Ideally, I’d want the evidences to come last. The usage will be the same, but it will be better supported by IntelliJ. As of this declaration, unfortunately, only Metals is able to provide useful autocompletions.
2 Likes
I don’t know the full details and it’s become more complicated in Scala 3 than Scala 2, but I think it essentially boils down to “type inference works from left to right”. When you provide an argument for parameter method, types A1 and R1 are fixed by the compiler. Then A1 =:= A2 and R1 =:= R2 are inferred which fixes A2 and R2. If the using parameters and stub were reversed than A2 and R2 would not be fixed yet when you pass in an argument for stub.
I don’t really understand why you need A2 and R2 though. Couldn’t you use PartialFunction[A1, R1]?
2 Likes
That makes sense. I can’t use just 2 type parameters since they will be inferred as Any, due to function variance, I believe. Unless I’m missing something obvious.
There were a couple of tickets recently that depended on how context bounds are desugared.
The spec says the evidence parameters are in one parameter list at the end, but IIRC they are prepended to the last parameter list if that is already implicit.
(No idea if it’s relevant here, but it was unexpectedly relevant on the ticket about default args.)
scala> class C[A: T](i: Int)(using String)
[[syntax trees at end of typer]] // rs$line$3
package <empty> {
final lazy module val rs$line$3: rs$line$3 = new rs$line$3()
final module class rs$line$3() extends Object() { this: rs$line$3.type =>
class C[A >: Nothing <: Any](i: Int)(using evidence$1: T[A], x$2: String)
extends Object() {
A
private[this] val i: Int
private[this] given val evidence$1: T[A]
private[this] given val x$2: String
}
}
}
// defined class C
1 Like
I would be surprised if context bounds are relevant here. This is still a mystery to me.
1 Like