Build "mpsc::Sender<B>" from: mpsc::Sender<A>, f: Fn(B) -> A

I'm referring to https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html

I have

sender: Sender<A>,
f: Fn(B) -> A,

Is there a way to build a

x: Sender<B>

from this?

Logically, the idea is that we intercept on the send, take the B, call f on it, get an A, then send the A instead.

No, there isn't a way to do this. One reason for this is because you can't efficiently make a Sender<B>, because you would need to type erase the Fn(B) -> A. You can make a combinator like so,

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=05facb11973280eb013b105bba4769d9

2 Likes

The only way here is to create fresh new (Sender<B>, Receiver<B>) pair, spawn a new thread, and pipe the Receiver<B> to the Sender<A> via impl Fn(B) -> A, and use the Sender<B> you've made.

1 Like

Changing my original question a bit:

What, if any, is the advantage of having a Sender<B> over having a Rc<dyn Fn(B)> ?

Because, it's fairly easy to create a Rc<dyn Fn(B)> from a F: B -> A and a Sender<A>

So now, of course, I'm wondering if a Sender<B> has any advantage over a Rc<dyn Fn(B)> at all.

For me, I'll use neither Sender<B> nor Rc<dyn Fn(B)> but create a new struct that wraps over Sender<A> and generic F: Fn(B) -> A. No more allocation, no dyn, easier to reasoning, more performant (may not be significant), and more Rusty(personally).

So, like my first playground example?

Yeah, glad to see that we have similar preference here. But for me simply mimicking the Sender's own methods without extra trait tastes better.

I did it that way because then you can compose maps together, for example I can do sender.map(f).map(g)

I never mentioned this as a requirement in my original post, but I think there is a key difference between your playground post and a Rc<dyn Fn(B)>.

I have the following weird situation:

crate Foo:
  has some function that generates B's
  and wants to send/callback on these B's
  imports struct B;
  does NOT import struct A;

crate X:
  imports A, B;
  has function F;
  calls function from crate Foo

If I am understanding the Playground example correctly, it seems that the location that generates the B's, needs to construct a Map<...>, which implies it needs to import A -- is that correct?

Again, I never mentioned this as part of the original post.

Well, how else would you create the mapping function? You could use generics I guess, but that may not play well with type inference.

This doesn't seem unique to my implementation.

Maybe we have different Rc<dyn Fn(B)> solutions in mind. The one I am currently using is:

crate foo:
  some_func(callback: Rc<dyn Fn(B)>) {
    ... blah blah ...
    let b: B = ...;
    callback(b);
    ...
  }

crate X:
  fn test() {
    let a: Sender<A>;
    let f: Fn(B) -> A;

    let cb: Rc<dyn Fn(B)>  = |b: B| { a.send(f(b)); };

    foo::some_func(cb);

I believe the dyn indirection gets around the need.

Oh, I see what you mean! Sorry, I misunderstood what you were asking. In this case you can depend on the SenderExt trait instead of Sender or Map, either through generics or by dynamic dispatch.

crate foo:
  some_func(callback: impl Sender<B>) {
    ... blah blah ...
    let b: B = ...;
    callback.send(b);
    ...
  }

Is

some_func(callback: impl Sender<B>)

the same as

impl<T: SenderExt<B>> some_func(callback: T)

?

So in particular, if we have

(Sender<A1>, F1: Fn(B) -> A1)
(Sender<A2>, F2: Fn(B) -> A2)

does the above generate two functions for some_func or only one?

I don't care about the extra code size. I'm trying to understand is this dispatch is static or dynamic.

Yes, it will generate 2 instances. You can use dynamic dispatch to get around this to an extent, but you will have to specify the error type generically. This means that it will still generate 2 instances in your case. If you don't want that, then you will need to create a new trait that ignores the error type.