Difficulty with function pointers and parametric types

Here’s a short program that doesn’t work: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=487e658efc9aee9d47840d7ff6b65576

I want (a) to be able to dynamically call either write_hello or write_bye, (b) but I don’t want to want to tie either of these functions to stdout.

Can anyone suggest a way of achieving this?

Unfortunately, what you’re asking for are generic function pointers, which as far as I know don’t exist in nearly any statically typed language. But, generic’s sibling, dynamics (As in dynamic dispatch), can rescue us here:

use std::io::{Result, Write, stdout};

fn main() {
    invoke(write_hello);
}

fn invoke(f: fn(&mut dyn Write) -> Result<()>) -> Result<()> {
    let mut stdout = stdout();
    f(&mut stdout)
}

fn write_hello(w: &mut dyn Write) -> Result<()> {
    writeln!(w, "Hello!")
}

fn write_bye(w: &mut dyn Write) -> Result<()> {
    writeln!(w, "Bye!")
}

Playground
This is because dynamic trait objects erase types and instead simply assure the user that the data under their pointer implements said trait.

4 Likes

I don’t know what you mean exactly, because your invoke function currently requires that W = Stdout when it calls the given function pointer:

use ::std::io::{
    self, // Result, /* do not shadow prelude items */
    stdout,
    Stdout,
    Write,
};

fn main ()
{
    invoke(write_hello).expect("IO error");
}

fn invoke (
    f: fn(&mut Stdout) -> io::Result<()>,
) -> io::Result<()>
{
    let mut stdout = stdout();
    f(&mut stdout)
}

fn write_hello<W : Write> (w: &mut W) -> io::Result<()>
{
    writeln!(w, "Hello!")
}

fn write_bye<W : Write> (w: &mut W) -> io::Result<()>
{
    writeln!(w, "Bye!")
}
  • And as a side note, using a (generic) closure gives users more freedom:

    fn invoke (
        f: impl FnOnce(&mut Stdout) -> io::Result<()>,
    ) -> io::Result<()>
    {
        let mut stdout = stdout();
        f(&mut stdout)
    }
    

At call site, you can remain generic, with the following:

fn main ()
{
    fn condition () -> bool { ::std::env::args().len() == 42 }

    if condition() {
        generic_feed(
            write_hello,
            stdout(),
        )
    } else {
        generic_feed(
            write_bye,
            stderr(),
        )
    }.expect("IO error");
}

fn generic_feed<W : Write> (
    f: fn(&mut W) -> io::Result<()>,
    mut stream: W,
) -> io::Result<()>
{
    f(&mut stream)
}
  • and with a closure:

    fn generic_feed<W, F> (
        f: F,
        mut stream: W,
    ) -> io::Result<()>
    where
        W : Write,
        F : FnOnce(&mut W) -> io::Result<()>,
    {
        f(&mut stream)
    }
    

However, if invoke's function needs to feed either a Stdout or a Stderr to the callback / function pointer, then you will need dynamic dispatch,

  • either (the most flexible way) with a vtable like @OptimisticPeach has shown:

    #![deny(bare_trait_objects)]
    
    use ::std::io::{
        self,
        stderr,
        stdout,
        Write,
    };
    
    fn invoke (
        f: fn(&mut (dyn Write + 'static)) -> io::Result<()>,
    ) -> io::Result<()>
    {
        fn condition () -> bool { ::std::env::args().len() == 42 }
    
        if condition() {
            f(
                &mut stdout()
                // add vtable
                as &mut dyn Write // unified type
            )
        } else {
            f(
                &mut stderr()
                // add vtable
                // as &mut dyn Write // can be implicitly coerced
            )
        }
    }
    
    fn main ()
    {
        invoke(
            write_hello,
        ).expect("IO error");
    }
    
    fn write_hello<W : Write + ?Sized> (w: &mut W) -> io::Result<()>
    {
        writeln!(w, "Hello!")
    }
    
    fn write_bye<W : Write + ?Sized> (w: &mut W) -> io::Result<()>
    {
        writeln!(w, "Bye!")
    }
    
  • or, if you know all the input types (e.g., Stdout and Stderr), you can unify the types under an enum:

    use ::std::io::{
        self, //Result, /* do not shadow prelude items */
        stderr, Stderr,
        stdout, Stdout,
        Write,
    };
    
    /// Unified type (instead of `&mut dyn Write`)
    enum StdoutOrStderr<'a> {
        Stdout(&'a mut Stdout),
        Stderr(&'a mut Stderr),
    }
    
    /// Macro that will expand our generic function
    /// into the two wanted monomorphisations
    macro_rules! write_to_StdoutOrStderr {(
        $generic_f:path
    ) => ({
        use $crate::StdoutOrStderr::*;
        (|stdout_or_stderr| match stdout_or_stderr {
            | Stdout(stdout) => $generic_f(stdout), // one monomoprhisation
            | Stderr(stderr) => $generic_f(stderr), // another one
        }) // as fn(StdoutOrStderr) -> io::Result<()>
    })}
    
    fn invoke (
        f: fn(StdoutOrStderr) -> io::Result<()>,
    ) -> io::Result<()>
    {
        fn condition () -> bool { ::std::env::args().len() == 42 }
    
        if condition() {
            f(
                StdoutOrStderr::Stdout(&mut stdout()) // type unification
            )
        } else {
            f(
                StdoutOrStderr::Stderr(&mut stderr()) // type unification
            )
        }
    }
    
    fn main ()
    {
        invoke(
            write_to_StdoutOrStderr!(write_hello),
        ).expect("IO error");
    }
    
    fn write_hello<W : Write> (w: &mut W) -> io::Result<()>
    {
        writeln!(w, "Hello!")
    }
    
    fn write_bye<W : Write> (w: &mut W) -> io::Result<()>
    {
        writeln!(w, "Bye!")
    }
    

As you can see, you can very easily unify under the &mut dyn Write trait object type, or with an explicit enum

1 Like