-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement structural type member access #1881
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
Conversation
New scheme for implementing structural type member access.
val milka = newCow | ||
val leo = newLion | ||
leo.eats(milka) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test error suggests that import scala.reflect.Projector.reflectiveProjector
is missing in this test.
Use base types instead of implicits. This is more robust in the presence of type abstraction.
The new scheme does not use implicit resolution, which makes it simpler and more robust. The old scheme suffered from the following drawback:
|
I think this is ready for review now. Who wants to review? |
I'm happy to review. Maybe @nicolasstucki can also have a look, as it's related to |
case tp: TypeProxy => | ||
hasRefinement(tp.underlying) | ||
case tp: OrType => | ||
hasRefinement(tp.tp1) || hasRefinement(tp.tp2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't we mixing up the body of OrType
and AndType
here? To select foo
, foo
needs to be present in both sides of an OrType
, but only one side of an AndType
. In any case, we need some testcases for structural type selection on union/intersection types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs to be present on both sides, but one side might be a regular class member. As long as one side comes from a refinement, the access is reflection based. For an AndType, it's the other way round. A single class member on one side is sufficient for regular access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see! The AndType
case makes sense, the OrType
case seems suspicious but I don't think it matters since we no longer allow calling a method on a union type that matches more than one symbol, e.g. the following doesn't compile:
class Closeable {
def close(): Unit = {}
}
object Test {
import scala.reflect.Selectable.reflectiveSelectable
def f(closeable: (Selectable { def close(): Unit }) | Closeable) =
closeable.close() // error: value `close` is not a member of (Selectable{close: ()Unit} | Closeable)(closeable)
def main(args: Array[String]): Unit = {
f(new Closeable)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be interesting to see if this scheme can be extended to support repeated parameters.
@@ -26,6 +26,9 @@ object Definitions { | |||
* else without affecting the set of programs that can be compiled. | |||
*/ | |||
val MaxImplementedFunctionArity = 22 | |||
|
|||
/** The maximal arity of a function thta can be accessed as member of a structrual type */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: thta -> that
if (tpe.isDependent) | ||
fail(i"has a dependent method type") | ||
else if (tpe.paramNames.length > Definitions.MaxStructuralMethodArity) | ||
fail(i"takes too many parameters") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expand this message to say "Structural types only support methods taking up to ${Definitions.MaxStructuralMethodArity} arguments", to keep people from guessing.
@@ -0,0 +1,73 @@ | |||
package scala.reflect | |||
|
|||
class Selectable(val receiver: Any) extends AnyVal with scala.Selectable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call this ReflectiveSelectable
or something like that, it's easy to miss the prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to @smarter good findings, I have a concern about platform compatibility -- how can user use structural types in a platform-independent way(native, scala.js)?
If I have a library which defines a method below:
def f(x: { def get: Int}) = x.get
If structural types were used as it's implemented now, it will force one particular implementation, thus makes the code above platform(implementation)-dependent.
I'm thinking if it's possible to transform the method above to the following if the type is a structural type:
def f(x: Selectable @{ def get: Int}) = x.get
The explicit structural type requirement becomes annotation for type checking purpose, the type will be changed to Selectable
.
This way, the code above will be more portable and implementation-independent.
* If `U` is a value type, map `x.a` to the equivalent of: | ||
* | ||
* (x: Selectable).selectDynamic(x, "a").asInstanceOf[U] | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter to selectDynamic
is incorrect, should be selectDynamic("a")
.
* | ||
* If `U` is a method type (T1,...,Tn)R, map `x.a` to the equivalent of: | ||
* | ||
* (x: Selectable).selectDynamicMethod(x, "a")(CT1, ..., CTn).asInstanceOf[(T1,...,Tn) => R] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter to selectDynamicMethod
should be selectDynamicMethod("a", CT1, ..., CTn)
package scala | ||
import scala.reflect.ClassTag | ||
|
||
trait Selectable extends Any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to have following annotation:
@implicitNotFound("no Selectable instance found to implement reflective access to structural type ${T}. Forget to import scala.reflect.Selectable.reflectiveSelectable?")
To expand on what @liufengyun suggested, currently AnyRef { $REFINEMENTS }
-->
Selectable { $REFINEMENTS } I don't think we can do that in the library by itself, but maybe with some compiler magic? |
It seems that we don't need reflection to implement If we transform the method below: def f(x: { def get: Int}) = x.get to def f(x: Selectable @{ def get: Int}) = x.selectDynamic("get").asInstanceOf[Int] At the calling site, we can transform the call:
to the following: val x = Some(5)
val d = new Selectable {
def selectDynamic(name: String): Any =
if (name == "get") x.get else error
}
}
f(d) Following the approach above, it seems we don't need reflection at all. Of course, for backward-compatibility this is not a good idea. |
x3.asInstanceOf[Object], | ||
x4.asInstanceOf[Object], | ||
x5.asInstanceOf[Object], | ||
x6.asInstanceOf[Object]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we require functions with fixed sizes? Could we not return an object with an apply(args: Object*)
and then pass the args directly to the invoke
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current solution has the advantage that the result of a Selectable is a drop in replacement of the original call. If we bunched arguments in an array, this would mean we also have the change the calling context. So it's more complicated. Selectable has lots of restrictions anyway, so one more does not really matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
Other related question: is the limit of 7 parameters an arbitrary limit or is there some factor that influenced this limit?
The PR also fixes scala#1866, as shown by this test case.
Have a look at Gilles Dubochet's thesis and at the "WhiteOak" paper he discusses. The WhiteOak paper uses essentially what you propose to avoid reflection. But the details are hairy. So hairy in fact, that the publicly available WhiteOak implementation does not follow the scheme described in their paper and uses reflection instead. |
Thanks for the pointer @odersky , I was thinking I must have missed something there :) |
Three code examples from the paper that fail the reflection-based approach: Test1: compiles, but runtime error import scala.reflect.Selectable.reflectiveSelectable
object Test1 {
def g(x: Any{ def + (i: Int): Int }) = x + 2
def main(args: Array[String]): Unit = g(3)
} Test2: doesn't compile import scala.reflect.Selectable.reflectiveSelectable
object Test2 {
def g[T](x: { def f(a: T): Boolean }, t: T) = x.f(t)
g[Int](new { def f(a: Int) = true }, 4)
g[Any](new { def f(a: Any) = true }, 5)
} Test3: compiler crash import scala.reflect.Selectable.reflectiveSelectable
object Test3 {
def g(x: { type T ; def t: T ; def f(a: T): Boolean }) = x.f(x.t)
g(new { type T = Int; def t = 4; def f(a:T) = true })
g(new { type T = Any; def t = 4; def f(a:T) = true })
} Test4: compiler crash import scala.reflect.Selectable.reflectiveSelectable
object Test {
def g(x: { def f[T](a: T): Int }) = x.f[Int](4)
} I don't think these cases are important, no one will ever write such code. Just document here for information. Edited: added Test4. |
@liufengyun Can you make an issue from Test 3? we should track that one. |
I believe they would each have different implicit implementations of |
@liufengyun @smarter Regarding the meaning of For the moment I would leave structural creation rules unchanged, waiting for learning more about the issues. |
Yes, if a library is specially designed for |
That's precisely what I suggest: Change the implementation of |
A previous type comparison was wrong because it did not map refined-this types. I believe it was also redundant, so the easiest fix is to drop it.
We can't handle them with the proposed scheme. I made a note in scala#1886.
Any more suggestions, or can we merge? |
New scheme for implementing structural type member access. The main motivation is to use one scheme that works for Java reflection as well as for user-defined access methods, such as accessing database fields where rows are represented as hashmaps.
Details will be explained in a separate issue.