Not sure how many people may find it useful, but idea is to make not only builder-like methods signatures convenient and type-safe (as they are after introduction of this.type
), but also prototype-like factory methods signatures (such as java.lang.Object.clone method)
So proposal is to define this#type
and constructor constrains like T <: {this(payload: String)}
and similar Self-type constraints using constraint-defining structural type. (Indirect invocation of bound in constraint constructor may look like val foo: T = ... ; new (foo#type)(payload = "bar")
)
This can be demonstrated by the following code snippets:
// type safe java.lang.Object.clone method signature
trait AnyRefEx {
abstract override def clone(): this#type = super.clone().asInstanceOf[this#type]
}
// describe type that impose some constraint on non abstract class to have appropriate 1-arg constructor
type HasStringConstructor = {
def this(payload: String)
}
Usage of this definitions may look like this
class Foo(val payload: String) {
// this: {def this(payload: String)} =>
this: HasStringConstructor =>
// factory methods with type-safe call to appropriate constructor of `this.getClass`
// which is required to be there by `this: HasStringConstructor =>` constraint
def `new`(val payload: String): this#type = new (this#type) (payload)
}
class Bar(payload: String) extends Foo(payload) with AnyRefEx with Cloneable {
// this: {def this(payload: String)} =>
this: HasStringConstructor =>
override def toString = s"Bar(payload=$payload,hash=${Objects.hash(this)})"
}
def testFooBar(): Unit = {
val foo = new Bar("text")
val bar = foo.clone() // bar.type <: Bar
val baz = new (foo#type)("baz-text") // baz.type <: Bar
val baz2 = foo.`new`("baz2-text") // baz2.type <: Bar
// should print: foo = Bar(payload=text,hash=1136497449) , bar = Bar(payload=text,hash=863125071) , baz = Bar(payload=baz-text,hash=1693847691)
println(s"foo = $foo , bar = $bar , baz = $baz")
// should print: bar.payload = text , baz.payload = baz-text , baz2.payload = baz-text
println(s"bar.payload = ${bar.payload} , baz.payload = ${baz.payload} , baz2.payload = ${baz2.payload}")
}
In other words, it would be nice to have this#type
(or more widely foo#type
) very similar to this.type
(foo.type
) but which rather “refers” to runtime type of this
(or any val foo
) instance.
It may look like this.type
is also fit well at least for typing clone-like factory methods, bat as for my understanding correct meaning of foo.type
can be expressed like bar.isInstanceOf[foo.type] <==> (bar == foo)
,
proposed foo#type
in general should be more close to foo.getClass
so that classOf[foo#type] == foo.getClass
and bar.isInstanceOf[foo#type] <==> foo.getClass.isInstance(bar)
So I would like to use correct signature of java.lang.Object.clone
, which should/could be def clone(): this#type
. And I would like to be able to write some prototype-like factory methods with signatures like def makeNew(...): this#type
.
Currently it is actually possible to use this.type
in signatures of that factory methods, and then just brutally cast to this.type
(in method body) which will look OK in place of usages of that factory methods, but it will be actually “not much” type-safe in place of their implementations, and as I underhand it definitely violates correct meaning of this.type
.
So here is that kind of working example which mimics previous code snippets using this.type
+ reflection + unsafe casts
trait AnyRefEx {
abstract override def clone(): this.type = super.clone().asInstanceOf[this.type]
}
class Foo(val payload: String) {
// construct new instance of the same type as this
def `new`(payload: String) : this.type = {
this.getClass.getConstructor(classOf[String]).newInstance(payload).asInstanceOf[this.type]
}
}
class Bar(payload: String) extends Foo(payload) with AnyRefEx with Cloneable {
override def toString = s"Bar(payload=$payload,hash=${Objects.hash(this)})"
}
def testFooBar(): Unit = {
val foo: Bar = new Bar("text")
val bar: Bar = foo.clone()
val baz: Bar = foo.`new`("baz-text")
// should print: foo = Bar(payload=text,hash=1136497449) , bar = Bar(payload=text,hash=863125071) , baz = Bar(payload=baz-text,hash=1693847691)
println(s"foo = $foo , bar = $bar , baz = $baz")
// should print: bar.payload = text , baz.payload = baz-text
println(s"bar.payload = ${bar.payload} , baz.payload = ${baz.payload}")
}
Also I should mention that in contrast to C# new constraint I would like to invoke that (defined in constraint) constructor of unknown at compile-time type T
not from T
itself, but form some instance of T
, so that if in C# it should be invoked as new T()
, here proposed invocation should look like val foo: T = ...; new (foo#type)
On bytecode level I would assume that instead of going to reflection and finding some appropriate constructor there at runtime, it rather should use some hidden (non static) method with name like “$new” and same argument list as in appropriate constructor from constraint (in all non abstract subclasses this method just need to be overridden with correct implementation for that sub-class).
Then new (foo#type)(arg1, ... , argN)
should be translated to foo.$new(arg1, ... , argN)
From the other perspective, similar correct typing (but without “built-in” factory methods) can be achieved by constructs like
trait HasThis {
type This <: HasThis
}
trait HasThis1[This <: HasThis2[This]]
And then used like
class Foo extends HasThis {
type This <: Foo
}
class Bar extends Foo {
type This <: Bar
}
class BarImpl extends Bar {
type This = BarImpl
}
Actually from this#type
I would expect similar behavior to that behavior of This
abstract type (from snippets above). But with that exception, that this#type
should be “refined automatically” (to eliminate that need to refine that abstract type This
in each subclass manually)
So that it would be possible to rewrite about snippets like
trait HasThis {
type This = this#type
}
And then
class Foo extends HasThis {
// it should be true that `This <: Foo`
// following method should be type-safe, however without imposing constructor constraint, it still can fail at runtime
def `new`(): this#type = this.getClass.newInstance()
}
class Bar extends Foo {
// it should be true that `This <: Bar`
}
class BarImpl extends Bar {
// it should be true that `This <: BarImpl`
}
In above snippet it was also assumed that actual return type of this.getClass
should become Class[this#type]
, so that inferred type of expression this.getClass.newInstance()
should be this#type