Polymorphism vs. Into Traits


#1

Please stay with me for a short intro. I have been recently trying to connect two pieces of software. One produces messages with certain attributes, the other contains different functions to consume them. Which function to call depends on the message. But not all functions in the second one necessarily take the full message as argument. Some functions take only some particular attribute or a few of them. Additionally, users might want to write their own. And I did not like the solutions I came up with.

Instead of showing the full code, I have written 4 small programs to exemplify. If you take a look at: https://is.gd/w5ybwW you will see a Dispatcher with a HashMap that links a keyword to a Fn. In this toy example, the key word is given in the main and the Fn is much simpler than the real stuff. What follows are 4 functions (sum, offset, constant, fcast) that might be part of the second library or written by the final user.

In the main, the user registers the functions by wrapping them into a closure. Then there are a few calls exemplifying what the system will do when it receives the messages with the payload (the key, x, y).

:thumbsup: Simple Dispatcher API to the final user: a single function called register
:thumbsup: Extensible: You can easily register a Fn with a different signature by wrapping it.
:thumbsup: Wrapping is done only once (at registering)
:thumbsdown: Wrapping can be annoying when there are more arguments or if a more involved casting is required.

So I decided to tackle the only thumbs down (and I failed). The first idea could be to add specific registration functions as seen here: https://is.gd/6iHLWh
But results in a complex API and extending requires adding methods to the Dispatcher impl

Another options was to create a rust Enum to contain all cases as shown here: https://is.gd/FvxRtR
The Dispatcher API has now a single function. But creating the Handler object is ugly, extending requires reimplementing the call method of the dispatcher and hacking the Handler Enum. Additionally, I have now to match in every single call.

What I would really like to do is something like this: https://is.gd/aBYxfW (but implementing the IntoHandler trait for the different function signatures). It provides a simple Dispatcher API, very easy to use and trivial to extend, even in other libraries. However, according to the answers to my previous question is not possible. But more over, some even commented that “fake overloading is the wrong approach”.

So my questions are:

  • One practical: What would be “the right approach” for this problem?
  • One conceptual: When is using traits like Into<String> (which also fakes overloading) ok?

If you are still there after all this long text, thanks!


#2

It’s hard to see what the problem is, but as a starting point what about pattern matching on the message for dispatch, which also solves the accessing parts of the message problem by deconstruction.


#3

The problem is finding a good, flexible way to make a dispatcher without having to manually wrap every single function to be called. I would like to have a way in which a function in wrapped when its registered according to predefined rules based on its signatures.

As a more general question, I would like to understand while using something like this is faking polymorphism and considered not ok (if this is really the case), but using an Into<String> is fine.


#4

I still don’t understand, you are creating more code and complexity by registering the functions and you still need to provide a mechanism to decide which function to dispatch to. It would be much simpler to have an enum for all the message types and then pattern match on the enum using deconstruction.

I don’t see any reason in the scenario you have presented this far that requires run-time registration.


#5

In my case, I have messages arriving and I need to dispatch them to different functions (some I have to write and others which have already been written and not necessarily conform a specific signature). I do not have just one application, but many, and each might change based on client demands. Making a way to register programmatically makes some things easier.

And sorry but it is not such a weird idea. People writing web apps might have the same needs. They have a web server that produces certain data structure with the request and you need to return another data structure (i.e Response). For each url you couldregister a Fn(Request) -> Response function. (See for example Iron Router).

Here is exactly the same, but I want to teach the router (in my case the Dispatcher) to wrap functions with other signatures to reduce the boilerplate. For example, consider that you want a web service that does image processing. If you already have a library with a lot of functions with the signature Fn(Image) -> Image, wrapping those makes things easier. I understand that I could write a function or a macro that return a wrapped function, but it will be annoying if I have more than one signature (i.e. a Text processing Fn(String) -> String, etc)


#6

To write a web server I would have a trait describing the interface for a web service, which would be passed a request and return a response. All services would implement this trait, I would then have a map from URL to trait-objects, and simply fetch the trait object and call the trait function API.


#7

Could you define a trait or a structure if there is only 1 implementation like:

trait Args {
   fn get_x() -> f32
   fn get_y() -> f32
}

With a register function like:

fn register<S, F>(&mut self, name: S, func: F)
    where S: Into<String>,
    F: Fn(&Args) -> f32 + 'a

Then you let the functions grab what they need from the object?


#8

I was also thinking about just a structure with public members, but I guess the way I’m approaching the problem is to normalize the arguments of the functions. It sounds like you want the functions to be whatever, and the dispatcher to hide the differences…That will require more thought.


#9

Thanks for the suggestion. This is differently from the way that iron router is doing it. There your define just a function, which seems to be more direct. Additionally, this does not really solves my problem. Your map from URL to trait-objects is like my register method. And what I am trying to do is to register (i.e. store in the mapping) functions which different signature from which I have previously defined a proper wrapper.


#10

You want overloading, which can only be done via traits in Rust, and you want functions with different type signatures, which cannot really be done in Rust at all. You will have to think about the problem differently.

You want services that are passed a request and return a response. You want those requests and responses to be of different types. To me this suggests the request and response should be enums, then every service has the same type: fn(x : RequestType) -> ResponseType. Inside each service you pattern match on the possible request types, and return an error response if it is not one you want, then you process the request. If you want to allow services to be chained together the request and response should both be the same type.

Why is this the right way to do this? Static type information implies you know the type at compile time (or at least the trait). Your system will not know the type of the message until runtime. There are two ways to do runtime polymorphism in Rust, trait objects where all the types have the same interface (whuch is what you want for your services so you can load new ones at runtime) and enums which can have different types, but you cannot extend with new types at runtime. You want this for your messages because you won’t allow a new type of message to be added at runtime (otherwise the services won’t know all the types of message they might receive). I hope that helps.


#11

It does helps. Thanks a lot. But I think that your solution is more inconvenient from the API exposed to the library user as can be seen in the code that I have linked in my first post. You said that overloading is done in rust via traits. Into traits is, I think, a way to accomplish overloading. And that is exactly how I was trying to accomplish this. I am trying to create a Into-like trait to wrap a function with a certain signature. But I was told that “fake overloading is the wrong approach”. On top, building an Into-like trait to wrap a function does not work.


#12

You should be using ‘enums’ for runtime polymorphism for this kind of thing. Put all the message types in an enum and then have the ‘service’ function take and return this enum. This is the nicest way to do this. How is this hard for the library user? If I want to write a new service I have a clear function signature I have to implement, and I know all the message types I have to handle or can generate, this sounds like a well defined API to me.


#13

Here’s an implementation to think about: https://is.gd/G0ehlL

By using the enum, when we extract the data from the HashMap we can use the enum to recover the type information for the function, hence the compiler knows what arguments ‘f’ accepts inside the ‘if let’ blocks. This makes it type-safe, the compiler won’t let you pass the wrong argument types, even though you don’t know which function you will actually be calling until runtime. The enum just needs to encode the argument patterns you want to allow, not every function. So in this case add, sum, mul, div etc would all use FN::A32A32R32.


#14

Indeed that is the way I thought it could be done (and it is shown in my third linked example of my original post). However, I still think using an Into-like traits is cleaner for the user (as seen in my nonworking 4th linked example). Sorry for my stubbornness and thanks for your time! I do appreciate.


#15

I don’t think that can work, as something has to record the types of the functions. There is no run-time-type-information or dynamic casting in Rust. The closest thing to this is the ‘Any’ trait.

It seems to be you are passing every function two arguments, so why not give every function the signature (f32, f32) -> f32, and let the function internally ignore the arguments it does not need.

Another alternative might be to pass the service a slice of boxed ‘Any’ trait, which you can pattern match on the argument inside the service.