Having statically typed access to dynamic data structures sounds great.
Is there a writeup somewhere about why we need old-style structural types at all?
I can’t speak for everyone, but my experience with structural types was poor from the outset, which is why I never used them. There were two related cases where they seemed handy.
- Unifying disparate logic in a type-safe but semi-ad-hoc way, as an alternative to boilerplate-heavy typeclasses, especially when I don’t have control over what’s being given to me.
Unfortunately, this immediately ran into problems: differences that were irrelevant when writing code became showstoppers, like
def closeMe(closeable: { def close: Unit }): Unit = closeable.close
class A { def close: Unit = {} }
class B { def close(): Unit = { println("Closing!") } }
closeMe
only works on A
, not B
. This thwarts the use of structural types for this use case. If I have control of all the code, I can unify it. But the point here is to unify cases where I may not, so it’s a failure.
(This was literally the first thing I tried with structural types–I wanted to close things with a close method, but couldn’t reliably.)
- Efficient selection of arbitrary subsets of well-defined capability, again as an alternative to typeclasses.
This one is immediately sunk by the terrible performance. For example,
case class Vec2(x: Double, y: Double) { def L2 = x*x + y*y }
case class Vec3(x: Double, y: Double, z: Double) { def L2 = x*x + y*y + z*z }
class len(v: { def L2: Double }) = math.sqrt(v.L2)
This works, but it’s so slow compared to the original operations that it’s a terrible idea to use it. Instead, you should create a trait or typeclass that abstracts out the L2
functionality.
With two big strikes against, my decision about structural types was to basically never use them on purpose; and until .reflectiveCalls
went in, occasionally and very unwelcomely accidentally used them when doing new Foo { def whatever }
and then calling whatever
. There are lots of cases where structural types for ad-hoc abstraction make more sense than tedious boilerplatey typeclasses, but when structural types are slow and fragile (almost entirely because of implementation details) and typeclasses are fast and robust, typeclasses win every time.
So, anyway, for structural types I see this proposal as digging them yet further into the ground. Now, not only does the first example fail if you don’t get your parens to agree, this one fails too:
class C {
def close(retry: Boolean): Unit = ???
def close: Unit = {}
}
And the second use case, which could be rescued by some clever compiler work, is probably pretty much buried by requiring it to be library-level. (Maybe I’m mistaken and there’s a good user-level implementation.)
So I don’t really see the point of keeping old-style structural types around at all. Just make all the old syntax fail, and require the new syntax for a different imagining of structural types. E.g. Reflect { def close: Unit = {} }
.
Stuff like
class Foo { def customary(i: Int) = i + 1 }
val x = new Foo { def novel(i: Int) = i*i*i + 2*i*i + 3*i + 4 }
would no longer have anything to do with structural typing, but rather would be an (ordinary) anonymous subclass, with normal rules about how method calls work: either you can’t get at the method at all when it’s anonymous, or it’s just a regular method that you have access to as part of an implicitly-generated and therefore weirdly-named but absolutely-normal class type.
Anyway, my objection to the proposal isn’t about what it enables. I think what it enables is great. I just think it leaves behind a trail of historical crud in the language supporting a feature that causes more problems than it solves, and probably makes it harder to improve that aspect of the feature.