Avoiding generic parameter proliferation

Hello

I’m writing a library that’ll help with runtime configuration and re-configuration. I already have something workable ‒ so I can define what TCP port (or ports), where to log… and the program will create the TCP listeners and shut them down as the configuration changes, change logging locations, etc, with minimal code in the end application.

However, as the library needs to be somewhat flexible on what it accepts from the caller, and because I try to have it composable and with code reuse, the signatures become very hairy with generic parameters. If you look for example on this impl block, the type bounds are longer than the actual code.

This gets the thing done, but I feel like this lacks elegance, as Lord Downey would put it (and makes the compiler complain about too complex types).

So, my question is, is there a better way? Is there something I do wrong, or am I just stretching the limits too much?

Have you considered using Procedural Macros or just plain Macros instead?

You mean macros so I don’t have to repeat the type bounds, or macros to let the user of the library to generate code as they need it?

The first would probably help a bit, but not that much. Especially when the problem is not the length of the bounds, but figuring out how to write them in the first place.

As for the latter, I’m not sure how to make a nice interface using that. Currently, I can do this thing:

I just give it a function that extracts the relevant typed fragment from configuration, a function to handle one connection (or whatever else makes sense for that thing described in the fragment) and a name. It does all the plumbing.

I usually reach for macros when I see a lot of copy-pastish code around, but I don’t think my problem is copy pasting here. But maybe I just don’t know all the tricks macros allow me to do to build something equally convenient to use.

There’s also the possibility the whole idea of the library is braindead in the first place :innocent:.

I have some random thoughts/ideas:

  1. Use C: DeserializeOwned instead of for<'de> C: Deserialize<'de>
  2. There’s lots of Sync + Send + 'static repetition - maybe define your own trait that requires these bounds, blanket impl it for T: Send + Sync + 'static and then use your trait in the bounds. Trait aliases would’ve been nice but they can’t be used today in bounds.
  3. Along similar lines, maybe create more (I know, sounds weird) traits that provide a “bundle” of functionality you want. Right now the bounds are pretty low level pieces, mostly combos of std types. Perhaps they can be grouped into higher level pieces.
  4. Things like Scaled being a trait seem … a bit overkill, but maybe it’s fine and good and needed.
  5. Maybe you can use some trait objects for types that aren’t super important to monomorphize (eg you don’t expect any perf benefits). I believe the clap crate author did a similar exercise, swapping generics for trait objects, to cut down binary size/bloat and compilation time.

All that said, how does this all look from the user’s perspective? If type inference mostly makes this type soup invisible, then it’s probably ok; it makes your life harder, but that may be just the “cost of doing (super flexible) business”.

2 Likes

Once the implied bounds RFC is implemented (…it was accepted just over a year ago, and no progress ;p), I think you’d be able to completely eliminate the where clause, assuming those bounds are all copied from the definitions of ResourceMaker and TcpListen.