(This isn't a proposal, but a description of a common problem I have and ideas as to how the language can solve it. Maybe other people have this problem, too.)
The Setup
The following is an issue I've been running into a lot, which I wonder if the language could do something about. Let's consider an unhypothetical scenario:
I have a library. It does crypto. It can't pull in something like ring because it's meant to run on SOCs with crypto acceleration in silicon. Thus, I wind up defining dozens of traits with names like Rsa
, Sha256
, EccBuilder
, and so on, which are related by way of associated types. So far, this is a pretty standard Rust abstraction.
The problem is that the entry point for the library now has something like a dozen type parameters that both I, the library author, and my users, need to type out. We can agree that the following is line noise:
impl<Sha256, Rsa, Ed25519, Hmac, Aes, Rng, ...> for ...
where <lots and lots of bounds> { ... }
The "standard" solution is to write this all down as associated types of a trait:
trait Crypto {
type Sha256: Sha256;
type Rsa: Rsa;
// ...
}
Implementing this trait is a lot of boilerplate and requires creating a single-use ZST, which isn't too terrible, but can be a bit rough to do when you want to capture impl type parameters:
struct MyCrypto<Rsa>(PhantomData(fn() -> Rsa));
impl<Rsa> Crypto For MyCrypto<Rsa> {
type Sha256 = MySha;
type Rsa = Rsa;
}
impl<Rsa> ... {
fn foo() -> MyStruct<MyCrypto<Rsa>>; // Requires T: Crypto.
}
Of course, the real version of this is worse, with many more type parameters.
A Potential Solution
I want to make this kind of API more ergonomic. I run into it a lot in code I need to write, and I can imagine code that has a lot of injection points (something like petgraph comes to mind) also runs into this problem.
This problem reminds me of the problem that Java's anonymous classes solve, so I tried to take that idea, apply it to this problem, and take it to its logical concluison.
What I would like to be able to write would be something like this:
impl<Rsa> ... {
fn foo() -> MyStruct<impl Crypto { type Rsa = Rsa; type Sha256 = MySha; }>;
}
The production impl $ident { $items }
in type position is intended to desugar into:
// In "scratch space", at item scope.
struct $anon;
impl $trait for $anon {
$items
}
// In type position.
$anon
Of course, if $items or $trait reference any names of types or lifetimes in scope, those should be captured as type parameters on $anon. In the example above, Rsa
is captured from impl
scope.
One could imagine that impl<'a> Trait<'a> { .. }
would result in a type implementing for<'a> Trait<'a>
.
As far as I know, this isn't the kind of thing we can really do with macros (locally), because it's not possible to inject a temporary scope in which to define items while in type position.
I also feel like this is a focused solution that only solves one problem. I'm not sure how something like this could be cleanly generalized.
This general problem reminds me of Scala's "type lambdas", though those are more in the space of "I have an HKT parameter, I want to make a type constructor that fits it on the spot". The syntax is as follows:
class Foo[T[_]] { .. } // Foo has kind (type -> type) -> type.
def x: Foo[[V] => Map[String, V]] // Curried application of Map[_, _]
This isn't the same kind of problem (since Rust doesn't, and seems unlikely to ever get, true HKTs), but it does dig into the problem space of "I need to define a fairly simple type on the spot". The solutions all feel like they want to be some kind of "block expressions, but for types". Both my "anonymous impl" and Scala's type lambdas feel like they're getting at this concept.
Thoughts? Maybe there's something useful here for improving the ergonomics of complicated, type-level code.