-
Notifications
You must be signed in to change notification settings - Fork 205
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
Implicit constructors in receiver/wrapper classes #309
Comments
One issue that needs to be addressed is cascades: (e1..foo()..bar()).baz() Here We usually say that we evaluate If we require the cascade receiver itself to be wrapped only once, then we have to either make an arbitrary choice of which single invocation to use for the receiver class wrapping, or to check for a single receiver class which satisfies all invocations (and fail if there is none). I think the former behavior is more reasonable. An extension method should act as if it was on the object, so it should be possible to mix it in between actual class methods. That also suggests that keeping updatable state between invocations is not something we can rely on, and not something we should be designing this feature around. |
@lrhn wrote:
Right, I hadn't spelled that out in detail. The specification of cascade execution actually describes it in such a way that we could claim that it is already being desugared as follows: e..suffix -->
let t = e, _ = t.suffix in t which will allow us to apply a separate implicit wrapping operation on each method invocation in a cascade (noting that This would mean that there is no shared state across the sections of a cascade. I believe that this would in fact be the most practical and comprehensible semantics.
I think it would be really confusing for developers if a cascade were to prevent regular instance methods from being invoked in later sections if the first one calls an extension method, and vice versa. And the above desugaring actually allows them to be mixed freely (instance methods and extension methods, from the same or from different extensions). Everything happens one cascade section at a time—this is both possible to understand and (presumably) useful in practice.
Right, we wouldn't and shouldn't have that for a cascade. On the other hand, I don't see any problems in having mutable state in a wrapper object in general. For instance, one extension method could give rise to a computation where any number of other methods on the same wrapper object would be executed, and they would be able to use the state of that wrapper object because it's in scope. For a developer who is writing a receiver class |
Can anyone tell me, about the progress made so far on this issue? This will be an exciting update for me. |
To add to that, what's the status of this vs extensions (and the proposed static extension types)? |
Static classes are probably not going to be added to the language. I think it would be a useful generalization of Implicit constructors themselves are mentioned now and then, also in connection with views (which is the new and better name for static extension types ;-). I think there is some support for the usefulness of the basic idea that we can mark a constructor as |
In response to #107 and #40, and as a foundation for solutions similar to #41 and #42, this issue proposes implicit constructors in a class which is either a receiver class or a wrapper class. The modifier
receiver
respectivelywrapper
on the class has just one effect: It controls the situations where an implicit constructor can be invoked.An implicit constructor must be a generative constructor. It only differs from other generative constructors by having the modifier
implicit
.The difference between implicit constructors and other constructors only arises at locations where they are invoked: Constructors must in general be denoted explicitly (e.g.,
C()
orC.name()
may create a new instance of the classC
, and the syntax explicitly allows us to look up the constructor namedC
respectivelyC.name
and see that this will create a new object). Implicit constructors can also be invoked explicitly.However, an implicit constructor invocation can also arise because a certain situation exists for an expression
e
, and that expression is then transformed into an expressione2
which differs frome
by invoking an implicit constructor withe
as an argument. For instancee.foo()
might be transformed intoC<int>.name(e).foo()
whereC.name
is an implicit constructor.The experience from Scala suggests that it is prudent to be cautious when introducing a mechanism that is capable of implicitly transforming objects of one type into objects of another type (see, for instance, this comment). With that in mind, this proposal only allows for creation of new objects by wrapping an existing object in a new one, in the sense that the existing object is passed as a constructor argument when the new one is created (and it would be surprising if it were ever useful to ignore that argument, so it's probably going to get "wrapped" in the new object in some sense).
However, the basic idea is similar: During static analysis it may be the case that an expression
e
of typeT
is used in some context where an instance ofT
cannot be used, ande
may then be implicitly replaced by an instance creationC<T1..Tk>(e)
which will work in that context.One such situation is simply when
e
occurs in a context where a typeS
is expected; this situation is handled with awrapper
class, as described below, and in this situationC<T1..Tk>(e)
is a subtype ofS
. The notion of awrapper
class can be considered as a foundation for (and generalization of) the notion of static extension types.Another situation is when
e
is used for a member access, likee.m(...)
, but the static typeT
ofe
has no member namedm
. In that situation we may again replace this expression byC<T1..Tk>(e).m(...)
, whereC
does have a member namedm
. This can occur when the classC
is areceiver
class. The notion of receiver classes can be considered to be a foundation for (and generalization of) the notion of static extension methods.There may be additional invocation criteria of interest, but this proposal starts off focusing on just these two: Wrapper classes and receiver classes.
The two proposals in the following can trivially be combined, this just amounts to allowing both class modifiers in the same program, and potentially on the same class. All rules about such classes will coexist without conflict.
Note that these proposals include the static class proposal (#308), because it is important for a receiver class and for a wrapper class to be able to be static, in order to ensure that extension methods can be invoked with just the cost of a top-level function invocation, and a static extension type can be used on a local variable without allocating a wrapper object for the target.
Proposal, Shared Part
Syntax
The grammar is adjusted as follows:
The only change is that it is possible to use the modifier
implicit
on a constructor.Static Analysis
An implicit constructor is a constructor whose declaration includes the modifier
implicit
.It is a compile-time error for a constructor to be implicit, unless it is a generative constructor.
It is a compile-time error for a generative constructor to be implicit, except in the cases that are explicitly allowed in the proposals below.
The static analysis of an implicit constructor proceeds identically to the static analysis of the same constructor with no
implicit
modifier.An implicit constructor only differs from the same constructor without
implicit
by being invoked implicitly in certain locations in code. This is specified as a source code transformation, and the normal static analysis is then applied to the transformed code.Dynamic Semantics
Implicit constructors have the same dynamic semantics as other constructors.
Again, call sites may be affected by the source code transformation mentioned above, but the dynamic semantics is unchanged when considering the code after this transformation.
Proposal for Wrapper classes
Syntax
The grammar is adjusted as follows:
The only change is that the modifier
wrapper
is allowed on a class declaration.Static Analysis
A wrapper class is a class whose declaration includes the modifier
wrapper
.The static analysis of a wrapper class is identical to the static analysis of other classes.
Expressions in the program are affected by the existence of wrapper classes:
Consider an expression
e
with static typeT
that occurs with context typeS
, and assume thatT
is not assignable toS
(without this proposal, that is a compile-time error).Assume that the set of wrapper classes in scope is
C1 .. Ck
. Letc1 .. cm
be the implicit constructors ofC1 .. Ck
accepting one positional argument (which means thatcj
is of the formSomeClass
orSomeClass.someName
for eachj
in1 .. m
), and assume that inference oncj(e)
with context typeS
succeeds and yields type argumentsUjs
(that is a list of actual type arguments, and the type listU1s
may have a different length than the type listU2s
, etc), forj
in1 .. n
.There may be fewer than
m
of these (because inference and/or type checking fails with some constructors), hence we go up ton
rather thanm
. We assume that the constructors have been ordered such that the failing ones are the ones with the highest numbers.If exactly one of
cj<Ujs>
is such that the type of its parameter is a subtype of all of those ofc1<U1s> .. cn<Uns>
thene
is replaced bycj<Ujs>(e)
. Otherwise a compile-time error occurs.That is, it is an error whenever multiple wrapper classes can be used (or even just multiple constructors from the same wrapper class), but none of them gives the target a "better type" than all the others.
Example
In the first case we infer
C1<num>(xs)
with context typeA
, and that yields the parameter typeIterable<num>
; withC2(xs)
(which is non-generic, so inference is a no-op), the parameter type isList<num>
.List<num> <: Iterable<num>
soC2
wins.Note that a class
C3
with a constructorC3(int i)
would be ignored, because it would be an error to passxs
as the argument to that constructor. SoC3
is out because the type check failed, and similarly an implicit wrapper constructor could be out because inference failed.In the second case we infer
C1<int>(ys)
, yielding parameter typeIterable<int>
, andC2(ys)
again yields parameter typeList<num>
. But none of those parameter types is most specific, so a compile-time error occurs.Of course, it is always possible for a developer to write
C1<int>(ys)
orC2(ys)
explicitly, thus eliminating the error by making that disambiguation decision that the compiler should not.Proposal for Receiver Classes
The grammar is adjusted as follows:
The only change is that the modifier
receiver
is allowed on a class declaration.Static Analysis
A receiver class is a class whose declaration includes the modifier
receiver
.The static analysis of a receiver class is identical to the static analysis of other classes.
Expressions in the program are affected by the existence of wrapper classes:
Consider an expression
e
with static typeT
which is subject to a member lookup for a memberm
, which we will indicate ase.m...
below. (*For instance, it could bee.m(42)
ore..m2()..m
), and assume that the interface ofT
does not have a member with the namem
. (Without this proposal, that is a compile-time error.)Assume that the set of receiver classes declaring a member named
m
in scope isC1 .. Ck
. Letc1 .. cm
be the implicit constructors ofC1 .. Ck
accepting one positional argument (which means thatcj
is of the formSomeClass
orSomeClass.someName
for eachj
in1 .. m
), and assume that inference oncj(e).m...
with context typeS
succeeds and yields type argumentsUjs
(that is a list of actual type arguments, and the type listU1s
may have a different length than the type listU2s
, etc), forj
in1 .. n
.There may be fewer than
m
of these (because inference and/or type checking fails with some constructors), hence we go up ton
rather thanm
. We assume that the constructors have been ordered such that the failing ones are the ones with the highest numbers.If exactly one of
cj<Ujs>
is such that the type of its parameter is a subtype of all of those ofc1<U1s> .. cn<Uns>
thene
is replaced bycj<Ujs>(e)
. Otherwise a compile-time error occurs.Example
In the first case we infer
C1<num>(xs).foo()
with context typenum
, and that gives the parameter the typeIterable<num>
; withC2(xs)
(which is non-generic, so inference is a no-op), the parameter type isList<num>
.List<num> <: Iterable<num>
soC2
wins.In the second case we infer
C1<int>(ys)
, yielding parameter typeIterable<int>
, andC2(ys)
again yields parameter typeList<num>
. But none of those parameter types is most specific, so a compile-time error occurs.Note that a receiver class
C3
that does not have a member namedfoo
is ignored, and so is a receiver classC4
that does not have an implicit constructor with one argument whose argument type is such that the type ofxs
orys
is assignable to it.Of course, it is again possible for a developer to disambiguate the situation by writing the invocation of one of the implicit constructors explicitly.
Static Variants
In the case where a wrapper class or a receiver class is static (cf. #308), the output from the code transformation associated with receiver respectively wrapper classes is such that the implicitly created object does not need to be allocated.
This makes it possible for static extension methods (#41) to be emulated by receiver classes:
In this desugaring, we rely on the ability of certain declarations to have the name
this
, which makes it possible to implicitly access members of the value ofthis
(just like we can access members of the current instance of the enclosing class in an instance method).In the case where the receiver class is not declared static, it is allowed for the "receiver object" to carry its own mutable state and to use references to itself in arbitrary ways (that is, all expressions are allowed rather than just the non-leaking ones, cf. #308). Similarly, it is possible for a receiver class to declare any number of constructors, implicit or not, and they can have superinterfaces and use mixins like any other class. These things make receiver classes a non-trivial generalization of static extension methods.
Similarly, it is possible for an extension type (#42) to be emulated by a wrapper class:
At call sites, a wrapped object for a static wrapper class can be accessed through desugared top-level functions taking the wrappee (and any instance variables of the wrapper) as actual arguments:
There may be small discrepancies. For instance, the use of
super
to invoke a method on the target object—the original receiver for a receiver class and the wrapped object for the wrapper class—does not work the same when using a receiver/wrapper class, because that will simply be an invocation of a method from a superclass of that receiver/wrapper class, butthis.foo()
would always call afoo
method on the original receiver/wrappee (rather than on the enclosing instance of the receiver/wrapper class), and a plainfoo()
would be used to get the one from the enclosing scope (and not from the original receiver/wrappee).Discussion
The purpose of considering receiver and wrapper classes as a foundation of extension methods and extension types is to express the desired semantics of such mechanism using the smallest possible increments:
Static classes are used in order to ensure that performance corresponds to the straightforward implementation strategies for extension methods (just desugar them to statically resolved function calls) and extension types (where the "view" provided by the extension type is applied to the target object without changing the representation of that target at all, that is, without having a wrapper: just call static methods that desugar the static wrapper class, and pass the wrappee as well as any "instance variables of the wrapper" as actual arguments to the static functions that are desugared versions of the wrapper class methods).
If we consider the ability to eliminate instances of static classes as a given, then we may think of other properties of these mechanism as if they always have those objects which are created by the code which is the output of the transformations.
So whenever a method is executed on an instance of a receiver class, it's just a completely normal method invocation and it is in principle unimportant that the original receiver is the value of a field in that receiver class instance.
Similarly, if an object is wrapped by an instance of a wrapper class and used in a context which is "leaking" (that is, it is not non-leaking, cf. #308) then we will obtain an actual instance of the wrapper class, and that object will implement the superinterfaces from the declaration of the wrapper class, which means that it is a full-fledged object of the desired type. So in the case where we actually get a wrapper object, it serves as a mechanism which provides the "view" of the extension type on the given wrappee object, and this property will hold in all context, e.g., also for dynamic invocations.
Comparing with #107 (and more general implicit conversion mechanisms), this proposal only addresses the situation where we can write a constructor for the target class (
B
in #107 lingo). For instance, it does not address the situation where we want to transform aString
into anint
, because we cannot makeint
a wrapper class.In relation to #108, this proposal also uses the modifier
implicit
on certain constructors. The obvious difference is that the situations where an implicit constructor can be applied is parceled out into thereceiver
case and thewrapper
case; I believe that #108 is mainly focused on the latter.The current proposal for wrapper classes does not support the "inverse conversion": We may implicitly wrap an
A
and get aB
, but there is no standardized way to obtain the originalA
again from thatB
, say, if theB
is passed around and anA
is needed somewhere. Indeed, nobody enforces that the construction of theB
even uses the argument of typeA
that it receives. It might be useful to have a standardized member in wrapper classes which are used to perform this kind of "unwrapping".The text was updated successfully, but these errors were encountered: