A couple of months ago, I took some time to assess the state of structural typing in Scala 3.3.0. My feeling is that the language could benefit from the addition of structurally typed records with a bespoke syntax for both types and terms.
Unfortunately, I don’t have much time currently to complete this investigation, so the purpose of this post is to:
- acknowledge the work done by Olof Karlsson and Philipp Haller
- share the status of my work
- see if there is any interest in continuing this work, both from the SIP Committee side and from the industry side
I will summarize my thoughts here, and I invite the interested readers to check out my full write-up on the matter.
Structural typing is useful when you want to manipulate structured data but you don’t want to define a class for it (e.g., because it is a temporary data structure used just once, which does not justify the “cost” of defining a class for it). Examples of Scala projects that involve structural typing are chimney, Iskra, Tyqu.
Today, Scala supports structural types via type refinements. As an example, here is a method divide
that returns a structurally-typed result:
def divide(dividend: Int, divisor: Int): Record { val quotient: Int; val remainder: Int }
In Scala 2, calling a structurally-typed member (e.g., calling .quotient
on the result of the method divide
) involves reflection, and is discouraged. Scala 3 provides a way to implement the type Record
in an efficient way that does not involve reflection. It relies on a special type Selectable
.
The special type Selectable
solves the problem of calling the members of structurally-typed values, but it does not address the problem of constructing or transforming structurally-typed values (e.g. concatenating two records). In 2018, O. Karlsson and P. Haller published a paper, Extending Scala with records: design, implementation, and evaluation, to address that problem. They proposed to add a very lightweight extension to the language (no new syntax, what they really introduced in the compiler was only the ability to synthesize implicits to compute the types resulting from the manipulation of records, and some rules to ensure the correctness of extending records). They also checked that the run-time performance of their Records
implementation was good.
My intuition was that their work did not get the visibility it deserves, and that the implementation of the aforementioned libraries involving structural types (chimney, Iskra, and Tyqu) would be simpler if extensible records were part of the language.
To check this hypothesis, I rebased their work on top of the main
branch of the Scala 3 compiler, and, after solving 5 years of Git conflicts, I tried to write minimal, record-based, re-implementations of chimney, Iskra, and Tyqu as tests.
My analysis is that only the chimney implementation would really benefit from the extensible records proposal. The benefits on Iskra and Tyqu’s implementations were quite marginal (more details in the full paper).
Another observation is that the extensible records proposal would not really improve the usability of those libraries. In my opinion, the main reason for that is that the syntax for writing record types and for manipulating record values is too verbose and non-idiomatic. See:
val point: Record { val x: Int, val y: Int } =
Record("x" ->> 1, "y" ->> 2)
Let’s compare that with tuples:
val point: (Int, Int) =
(1, 2)
Or case classes:
case class Point(x: Int, y: Int)
val point: Point =
Point(x = 1, y = 2)
The thing is that, the main purpose of structural types is to be used when defining a class is not possible or convenient. So, the workarounds such as defining a class or a type alias (type Point = Record { val x: Int; val y: Int }
) are irrelevant.
My main conclusion is that if we want to make structural types (or structurally-typed records) usable in Scala, they need to have a concise syntax.
In the paper, I propose the following syntax, which looks like tuples, but with named fields instead of positional fields:
val point: (x: Int, y: Int) =
(x = 1, y = 2)
def divide(dividend: Int, divisor: Int): (quotient: Int, remainder: Int) =
(quotient = dividend / divisor, remainder = dividend % divisor)
I believe more research is needed to validate that it would really work in practice and improve the usability.
My second idea is that a better support of generic programming (Mirror
s) with records could simplify the implementation of Iskra and Tyqu (and the other libraries that involve structural types), by providing automatic conversions between case classes and records, for instance (more details in the full paper). However, here too, more work is needed to validate this hypothesis.