Type aliases aren't simple replacements? + impl and closures signatures

I'll be referencing this playground: Rust Playground

My goal here is to be able to "schedule" a variety of objects and call them later, providing a way to schedule additional objects grabbed from the cache. the execution and cache based scheduling will happen in a "realtime" audio thread so I don't want to do heap allocation there. My Cache traits, for example CacheFoo, will indicate which types of objects I can schedule in the other thread.

I guess I'm wrong in my thinking that type X = Y; simply replaces Y with X wherever you use it?

On line 79 I call seq.schedule(Box::new(move |_s: &mut Sched<CacheImpl>|
which works but if I create type Impl = Sched<CacheImpl>; and then do, line 84,:
seq.schedule(Box::new(move |_s: &mut Impl>| { I can't compile.

Finally, on line 89, I'm wondering if I can write a closure that takes a object that impl's some trait? If all I care about is that my closure's argument implements a specific cache trait, can I reference it like that some how?

Thanks,
Alex

type Impl<'a> = Sched<CacheImpl> + 'a should make this work. Because you use a trait object at the use site, you need to make sure the type alias can infer the proper lifetime at use sites rather than defaulting to 'static.

Not super clear what you want here. But maybe I've not absorbed the full example yet ...

2 Likes

@vitalyd once again, thanks!! So does s: &mut Sched<CacheImpl> in a function signature imply s: &'static Sched<CacheImpl> or is it just when I use a type alias?

I've updated my playground example with your advice but hopefully also made my 2nd question a bit more clear.

On line 89:

Can I pass a closure that takes an object that simply implements the interface I require? In this case Sched<CacheFoo> ? Maybe my automatic SchedCall implementation for Fn isn't quite right for that?

In a function signature, it's like this:

fn foo<'elided>(s: &'elided mut Sched<CacheImpl> + 'elided, ...)

When you define a type alias, however, there's an implicit 'static. It's as if you wrote:

type Impl = Sched<CacheImpl> + 'static

When you then use the alias in an fn, the fn becomes:

fn foo<'elided>(s: &'elided mut Sched<CacheImpl> + 'static)

So the 'static carries over. When you define the alias as having a lifetime parameter, then 'elided carries over. It's quite subtle until you know about it.

I'll try to look at your revised playground a bit later ...

2 Likes

So I think the issue with the current code is SchedImpl impls Sched<CacheImpl>, and this makes schedule only accept CacheImpl based SchedCalls - you can't pass it something else.

Perhaps you can impl Sched<CacheFoo> for SchedImpl as well? Given the Sched trait is generic, that allows you to implement it multiple times for a given type and vary the generic type inside Sched.

Ahh okay.. I'm not sure if that'll do what I want because I would like to be able to use multiple cache impls in a single call (sorta psudo code, I want to be albe to simply specify that the Sched that calls me has a cache that implements these 2 pop methods):

seq.schedule(Box::new(move |s: &mut Sched<CacheFoo + CacheBar>| {
  let x = s.cache().pop_foo().unwrap();
  let y = s.cache().pop_bar().unwrap();
  y.schedule(x); //if y impl Sched or some sort that x requires
  s.schedule(y);
});

What I'm hoping for is a way to call s.schedule(object) where object requires that the s's cache implements some set of Cache methods but not necessarily some specific cache implementation, and do the same with closures.

Have you tried making Sched not generic? schedule() would then take a trait object, whose trait allows it to be called with a trait object of a Sched; Sched in turn exposes a Cache trait object, which I think you already have. All concrete types are erased.

I'm not 100% I follow @vitalyd.

The Sched trait has a schedule method which takes SchedFn which are boxed objects that have a method sched_call(&mut self, sched: &mut Sched) so that they can schedule additional objects. The objects that implement Sched will eventually store these SchedFn objects and call them later, passing a reference to either itself or some other object that implements that Sched interface. I'm thinking that the Sched trait needs to provide a cache() method that returns a reference to something that, in my later usage, I want to require implements certain traits. I'm not sure how to get around generics for Cache if I don't know what my cache will actually be until I need it?

I'm trying to create a library to use in a variety of projects down the line which may have additional sets of functionality included or not. For instance, I'm planning to implement MIDI scheduling and execution, so I can control synthesizers, my cache would need to be able to provide me with midi objects that I can manipulate and schedule, but I don't want to require MIDI in the base library because I believe the library will be useful for just generally saying "call this thing at time t" and all sorts of other situations like that.

By full erasure, I meant something like this. This is essentially how you'd implement this in a OOP language. Whatever functionality callbacks require of the Cache need to be part of that trait.

In your previous attempt, there are a couple of things going against you:

  1. If you use generics in trait fn signatures, those methods will not be callable via a trait object. So, for example, Sched::cache() cannot be invoked by the SchedCall because cache() returns the generic parameter.
  2. SchedImpl implements Sched for a concrete Cache type: CacheImpl. Since that type is tied to SchedCall provided in schedule, you cannot pass a different type. This is why I was suggesting implementing Sched on SchedImpl with multiple Cache types - that would allow you to call schedule with different objects (as long as they fit the Cache type you chose in the impl).

Mixing generics and trait objects is pretty painful because you always have to keep in mind the restrictions on trait objects, and define the API such that you stay clear of those restrictions.

Have you looked at any existing art in the Rust ecosystem? Crates that facilitate submission (i.e. internal storage, execution, marshaling to different thread, etc of caller supplied tasks)?

Ahh okay, thanks @vitalyd but I think the generic approach is actually what I'm looking for. The fact that my closures require a specific Implementation is not that big of a deal because my other types are able to require a specific cache type be implemented for their SchedCall implementation.

The hope for the Cache, which I believe I have with this generic setup, is to be able to write a library for Sched and surrounding functionality and then define what the Cache provides when I use the library. So I can add new types to the cache without modifying the library. For instance, for MIDI I'll want to provide a pop_midi() for the cache for getting and scheduling midi objects in the execution thread. But for implementations that don't require MIDI, I won't need to have all the supporting code for it, and won't have the pop_midi() cache method.

I haven't found any prior art that matches my goals in the Rust ecosystem but I'm curious if you have any links?

Either way, thanks for all your help @vitalyd, it has been great!

Yeah, I think if your closures all require the same Cache type as the Sched provides, then it works. But it seemed like you wanted to add additional constraints on the Cache at submission time (ie schedule), and that’s the troublesome part.

By the way, maybe Sched doesn’t need to expose the cache at all? The SchedCall types get one somewhere else, prior to submission, and then use it inside the callback? Perhaps that would simplify the design a bit.

A couple that spring to mind are rayon and tokio - both maintain a submission queue of type erased objects. They of course don’t have your Cache like requirement but you may get some inspiration from their internals nonetheless. Maybe someone else reading this thread has other suggestions.

1 Like