Need heterogeneous list that's not statically typed

As part of my effort here:

I'm trying to port this JavaScript function to Rust:

However, I have not found a way to have a heterogeneous collection of Source<T> where it's a struct Callbag<I, O>(Box<dyn Fn(Message<I, O>) + Send + Static>):

I've read up about using trait objects, but from what I understand they don't support generic type parameters and/or associated types.

I've also found frunk's HList, but it only supports statically-typed heterogeneous lists: HList in frunk_core - Rust

That in itself is not true. You can make a trait object out of a Trait<T> or a Trait<Assoc=Foo> or a Trait<T, Assoc=Foo>. If that's all you need, you can create a collection of Box<dyn Trait<T, Assoc=Foo>>.

What you can't do is call generic methods on a trait object, because all methods need to exist by the time the trait object is created, since they need to be put in a vtable. If this is what you need, then indeed you can't use trait objects. In this case, you probably want to create an enum with a different associated value in each of its variants.

2 Likes

Can I create a collection of Box<dyn Trait<T>> where T is heterogeneous though, i.e. a collection that contains trait objects where their type T differ?

Using an enum is not an option, as I cannot know in advance which types of T there can be. (In my case, these are output types.)

Basically what I'd like to achieve is something along the lines of:

fn combine(sources: Vec<Box<Source<_>>>) -> Source<Vec<Box<_>>> {
    // ...
}

where _ is any output types (dynamic, i.e. not monomorphized).

No, you can't.

I don't understand what that JS is doing but it looks very... functional. Functions taking functions and returning other functions. Callback-based APIs and higher-order functions are often difficult to emulate in Rust because they're hard to anchor to the world of concrete, statically determined types.

You can probably make it work on some level by aggressively erasing every type, and when you have to erase a function type, wrap it in a function that takes dyn Any trait objects and downcasts internally when called. This is what other languages call "dynamic typing". But what is effortless in a dynamically typed language will be horribly clunky when you have to explain it to the compiler step by painful step. Even so, there will be things you just can't do that way because this quasi-untyped programming is not a well-developed part of Rust.

Trying to make Rust be all functional is often just as hard as trying to make it be all OOP. You may need to adjust your way of thinking to something that works with the language.

12 Likes

No. What are you trying to achieve with this? More often than not, violating static typing actually indicates a design error.

1 Like

I'd like to implement the combine operator, which takes a collection of Source<T> (where T is heterogeneous), and upon each source "emitting" an item (of type T), emit into the "sink" an item which is a collection (ideally a tuple actually, but there's no variadic tuple in Rust) containing all the items from the different sources.

It's as explained here:

Yes, indeed. This is entirely functional programming. My goal is to try to make a Rust port of Cycle.js, while learning Rust in the process.

2 Likes

You might be underestimating the difference in languages. JavaScript and Rust are quite different.

Think of having instructions on how to build a house with LEGO bricks. Now these instructions are handed to a carpenter. The instructions he got will be totally useless to him because they are in a different language than he uses.

Cycle.js tries to solve problems that JS has. Trying to solve these problems for a different language makes limited sense as it may well not have the problems you want to solve. Or lack the tools you took for granted.

11 Likes

Now you essentially pushed the question one level
further, and it now is: why is the source heterogeneous in the first place? I see you are trying to replicate a reactive framework. There, events and messages are usually known at design time, and as such, they are easy to represent with an enum.

2 Likes

I think I get what you're saying. Yes, indeed the output types T from the different sources (Source<T>) are statically known at compile time.

But is the type system in Rust expressive enough to represent taking a variadic tuple of sources with different T?

From what I can find, the answer seems to be no. That's why I'm stuck now.

they are easy to represent with an enum

I fail to see how this could work. The types are indeed known at compile time, but that's not the same as being a fixed set of source types. Unless there's variadic generics, this doesn't seem possible.

Or can a macro generate an enum type at compile time based on the types T of a set of sources provided? (Sounds like const execution to me. At least it reminds me of comptime in Zig.)

My understanding of this conversation so far is that you can do the above, as long as the methods in the trait are not themselves generic, as @H2CO3 said:

So it seems what you want to do with a dyn trait should work, you just need to be sure that no methods in the trait have generic types, meaning (I believe) that no method parameters have generic types. I suggest trying that.

If all else fails, perhaps look at std::any - Rust ?

(Cycle.js has a very clean design, I understand your desire to use it.)

2 Likes

You create the enum with one variant for each possible type implementing the trait you need. Then you implement the trait for the enum, as well and delegate the call to the the stored type.

Simple example:

trait TrA {
    fn f(&self);
}

struct StA;

impl TrA for StA {
    fn f(&self) {
        println!("<StA as TrA>::f");
    }
}

struct StB;

impl TrA for StB {
    fn f(&self) {
        println!("<StB as TrA>::f");
    }
}

enum EnA {
    A(StA),
    B(StB),
}

impl TrA for EnA {
    fn f(&self) {
        match self {
            A(a) => a.f(),
            B(b) => b.f(),
        }
    }
}

I believe the types that implement the trait are supplied by the library user, not by the code @teohhanhui is writing (the library). An enum cannot be statically defined by the library.

1 Like

That's exactly right. Users must be able to implement their own "callbags": sources, sinks, operators, etc.

Unless there's compile-time evaluation (constant evaluation) support for defining enum variants, using an enum can't be the solution.

Hmm... If you wouldn't mind, could you provide some code to illustrate how it could work? I've tried to read and re-read and still have no clue how that could be done. :bowing_man:

You may have to change your library interface so that everything that is "dynamic" about the caller's types are behind a dyn Trait interface rather than a generic type parameter. In other words, you may need to add more traits to the library interface. That's a very general suggestion and maybe it doesn't help, so I'll try to look more closely at your interface and come up with a code example.

2 Likes

How not to learn Rust, mistakes 3, 6, 6a, 6b, and 11.

Pretty high score, but it's not entirely hopeless, I would say. I wish you luck. If you would buy enough painkillers you may actually succeed.

4 Likes

@teohhanhui I apologize! I have created the rope to hang myself on this. I thought it would be possible to abstract the I and O types using more traits, but I have not been able to do this. Adding more traits just pushes the problem to the next level.

So I have to agree with the advice from @VorfeedCanal and others -- this is extremely difficult or impossible in Rust. I should not have been optimistic before actually trying this myself.

More specifically, I don't see how to have a heterogeneous collection of trait objects having a function that returns a type, unless that type is monomorphic.

1 Like

Of course I should have known this since it is stated explicitly in the book:

(Using Trait Objects That Allow for Values of Different Types - The Rust Programming Language)

Object Safety Is Required for Trait Objects

You can only make object-safe traits into trait objects. Some complex rules govern all the properties that make a trait object safe, but in practice, only two rules are relevant. A trait is object safe if all the methods defined in the trait have the following properties:

  • The return type isn’t Self .
  • There are no generic type parameters.