Declaring an associated type that holds references to self's fields

I initially had the following code for an Interface trait for use in displaying text to a user and getting input back. This includes a separate InterfaceProvider trait that passes a &mut Interface to a supplied function, making it possible to ensure that any necessary startup & cleanup code ran before & after using the interface. (I haven't yet implemented any interfaces that need startup or cleanup, but I'm thinking ahead.)

use std::io::{self, BufRead, Write};

pub trait InterfaceProvider {
    type Interface: Interface;

    /// Run `func` with the associated `Interface`.
    fn with_interface<F>(self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface) -> io::Result<()>;
}

pub trait Interface {
    /// Display the given text in the interface.
    fn show_output(&mut self, text: &str) -> io::Result<()>;

    /// Read a line of input from the interface.
    ///
    /// Returns `None` on end of input.
    fn get_input(&mut self) -> io::Result<Option<String>>;
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BasicInterface<R, W> {
    reader: R,
    writer: W,
    wrote_prompt: bool,
    wrote_last_output: bool,
}

impl<R, W> BasicInterface<R, W> {
    pub fn new(reader: R, writer: W) -> Self {
        BasicInterface {
            reader,
            writer,
            wrote_prompt: false,
            wrote_last_output: false,
        }
    }
}

impl<R: BufRead, W: Write> InterfaceProvider for BasicInterface<R, W> {
    type Interface = Self;

    fn with_interface<F>(mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface) -> io::Result<()>,
    {
        func(&mut self)
    }
}

impl<R: BufRead, W: Write> Interface for BasicInterface<R, W> {
    fn show_output(&mut self, text: &str) -> io::Result<()> {
        if self.wrote_prompt {
            writeln!(&mut self.writer)?;
        }
        if !text.is_empty() {
            writeln!(&mut self.writer, "{text}")?;
            self.wrote_last_output = true;
        } else {
            self.wrote_last_output = false;
        }
        Ok(())
    }

    fn get_input(&mut self) -> io::Result<Option<String>> {
        if self.wrote_last_output {
            writeln!(&mut self.writer)?;
        }
        write!(&mut self.writer, "> ")?;
        self.writer.flush()?;
        self.wrote_prompt = true;
        let mut input = String::new();
        if self.reader.read_line(&mut input)? != 0 {
            Ok(Some(input))
        } else {
            // Force the start of a new line:
            writeln!(&mut self.writer)?;
            Ok(None)
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StandardInterface;

impl InterfaceProvider for StandardInterface {
    type Interface = BasicInterface<io::StdinLock<'static>, io::StdoutLock<'static>>;

    fn with_interface<F>(self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface) -> io::Result<()>,
    {
        let mut iface = BasicInterface::new(io::stdin().lock(), io::stdout().lock());
        func(&mut iface)
    }
}

(Playground)

As you can see, InterfaceProvider::with_interface() consumes the provider, as I wanted to keep things simple and lifetime-free at first. Now, however, I'm ready to make things complicated and lifetime-full, but I've gotten a bit over my head.

Specifically, I want to make the following changes:

  • Change the receiver of with_interface() to &mut self (not &self, as the interface may borrow some data from the provider, and the interface has to be mutable, so the provider should be as well)

  • Split BasicInterface into:

    • a BasicInterfaceProvider that only implements InterfaceProvider and just stores the reader & writer
    • a BasicInterface that only implements Interface and stores mutable references to the provider's reader & writer plus the state fields
  • Change the Interface for StandardInterface to the new BasicInterface, still parametrized by stdin & stdout

My best (as in, producing the fewest errors) attempt so far has been as follows, where I changed the with_interface() receiver, split BasicInterface apart, and tried (and failed) to define BasicInterfaceProvider::Interface as a BasicInterface parameterized by the lifetime of self:

Attempt #1
use std::io::{self, BufRead, Write};

pub trait InterfaceProvider {
    type Interface: Interface;

    // CHANGED: Receiver changed from `self` to `&mut self`
    fn with_interface<F>(&mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface) -> io::Result<()>;
}

pub trait Interface {
    fn show_output(&mut self, text: &str) -> io::Result<()>;
    fn get_input(&mut self) -> io::Result<Option<String>>;
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BasicInterfaceProvider<R, W> {
    reader: R,
    writer: W,
}

impl<R, W> BasicInterfaceProvider<R, W> {
    pub fn new(reader: R, writer: W) -> Self {
        BasicInterfaceProvider { reader, writer }
    }
}

impl<'a, R: BufRead, W: Write> InterfaceProvider for BasicInterfaceProvider<R, W> {
    type Interface
        = BasicInterface<'a, R, W>
    where
        Self: 'a;

    fn with_interface<F>(&mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface) -> io::Result<()>,
    {
        let mut iface = BasicInterface::new(&mut self.reader, &mut self.writer);
        func(&mut self)
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct BasicInterface<'a, R, W> {
    reader: &'a mut R,
    writer: &'a mut W,
    wrote_prompt: bool,
    wrote_last_output: bool,
}

impl<'a, R, W> BasicInterface<'a, R, W> {
    fn new(reader: &'a mut R, writer: &'a mut W) -> Self {
        BasicInterface {
            reader,
            writer,
            wrote_prompt: false,
            wrote_last_output: false,
        }
    }
}

impl<R: BufRead, W: Write> Interface for BasicInterface<'_, R, W> {
    fn show_output(&mut self, text: &str) -> io::Result<()> {
        if self.wrote_prompt {
            writeln!(&mut self.writer)?;
        }
        if !text.is_empty() {
            writeln!(&mut self.writer, "{text}")?;
            self.wrote_last_output = true;
        } else {
            self.wrote_last_output = false;
        }
        Ok(())
    }

    fn get_input(&mut self) -> io::Result<Option<String>> {
        if self.wrote_last_output {
            writeln!(&mut self.writer)?;
        }
        write!(&mut self.writer, "> ")?;
        self.writer.flush()?;
        self.wrote_prompt = true;
        let mut input = String::new();
        if self.reader.read_line(&mut input)? != 0 {
            Ok(Some(input))
        } else {
            // Force the start of a new line:
            writeln!(&mut self.writer)?;
            Ok(None)
        }
    }
}

// Not yet sure how to declare the Interface associated type for StandardInterface

(Playground)

This fails to compile with:

error[E0207]: the lifetime parameter `'a` is not constrained by the impl trait, self type, or predicates
  --> src/lib.rs:29:6
   |
29 | impl<'a, R: BufRead, W: Write> InterfaceProvider for BasicInterfaceProvider<R, W> {
   |      ^^ unconstrained lifetime parameter

For more information about this error, try `rustc --explain E0207`.

I also tried the following, inspired by the unstable Pattern trait and its Searcher associated type. I changed the definition of InterfaceProvider::Interface to type Interface<'a>: Interface<'a>, changed the receiver for with_interface() to &mut self, added <'_> to the &mut Self::Interface argument to the FnOnce passed to with_interface(), split BasicInterface apart, defined BasicInterfaceProvider::Interface<'a> as a BasicInterface<'a, R, W>, and added some lifetimes to the bounds of impl InterfaceProvider for BasicInterfaceProvider based on the compiler's suggestions, but I still can't get it to compile.

Attempt #2
use std::io::{self, BufRead, Write};

pub trait InterfaceProvider {
    type Interface<'a>: Interface<'a>;

    fn with_interface<F>(&mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface<'_>) -> io::Result<()>;
}

pub trait Interface<'a> {
    fn show_output(&mut self, text: &str) -> io::Result<()>;
    fn get_input(&mut self) -> io::Result<Option<String>>;
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BasicInterfaceProvider<R, W> {
    reader: R,
    writer: W,
}

impl<R, W> BasicInterfaceProvider<R, W> {
    pub fn new(reader: R, writer: W) -> Self {
        BasicInterfaceProvider { reader, writer }
    }
}

impl<'a, R: BufRead + 'a, W: Write + 'a> InterfaceProvider for BasicInterfaceProvider<R, W>
where
    Self: 'a,
{
    type Interface<'a> = BasicInterface<'a, R, W>;

    fn with_interface<F>(&mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface<'_>) -> io::Result<()>,
    {
        let mut iface = BasicInterface::new(&mut self.reader, &mut self.writer);
        func(&mut iface)
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct BasicInterface<'a, R, W> {
    reader: &'a mut R,
    writer: &'a mut W,
    wrote_prompt: bool,
    wrote_last_output: bool,
}

impl<'a, R, W> BasicInterface<'a, R, W> {
    fn new(reader: &'a mut R, writer: &'a mut W) -> Self {
        BasicInterface {
            reader,
            writer,
            wrote_prompt: false,
            wrote_last_output: false,
        }
    }
}

impl<'a, R: BufRead, W: Write> Interface<'a> for BasicInterface<'a, R, W> {
    fn show_output(&mut self, text: &str) -> io::Result<()> {
        if self.wrote_prompt {
            writeln!(&mut self.writer)?;
        }
        if !text.is_empty() {
            writeln!(&mut self.writer, "{text}")?;
            self.wrote_last_output = true;
        } else {
            self.wrote_last_output = false;
        }
        Ok(())
    }

    fn get_input(&mut self) -> io::Result<Option<String>> {
        if self.wrote_last_output {
            writeln!(&mut self.writer)?;
        }
        write!(&mut self.writer, "> ")?;
        self.writer.flush()?;
        self.wrote_prompt = true;
        let mut input = String::new();
        if self.reader.read_line(&mut input)? != 0 {
            Ok(Some(input))
        } else {
            // Force the start of a new line:
            writeln!(&mut self.writer)?;
            Ok(None)
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StandardInterfaceProvider;

impl InterfaceProvider for StandardInterfaceProvider {
    type Interface<'a> = BasicInterface<'a, io::StdinLock<'static>, io::StdoutLock<'static>>;

    fn with_interface<F>(&mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface<'_>) -> io::Result<()>,
    {
        let mut stdin = io::stdin().lock();
        let mut stdout = io::stdout().lock();
        let mut iface = BasicInterface::new(&mut stdin, &mut stdout);
        func(&mut iface)
    }
}

(Playground)

This fails to compile with:

error[E0496]: lifetime name `'a` shadows a lifetime name that is already in scope
  --> crates/advcore/src/interface.rs:34:20
   |
30 | impl<'a, R: BufRead + 'a, W: Write + 'a> InterfaceProvider for BasicInterfaceProvider<R, W>
   |      -- first declared here
...
34 |     type Interface<'a> = BasicInterface<'a, R, W>;
   |                    ^^ lifetime `'a` already in scope

error[E0207]: the lifetime parameter `'a` is not constrained by the impl trait, self type, or predicates
  --> crates/advcore/src/interface.rs:30:6
   |
30 | impl<'a, R: BufRead + 'a, W: Write + 'a> InterfaceProvider for BasicInterfaceProvider<R, W>
   |      ^^ unconstrained lifetime parameter

Some errors have detailed explanations: E0207, E0496.
For more information about an error, try `rustc --explain E0207`.

I've tried minor variations on both of the above attempts, but I can't get anything that compiles. Help?

Disclaimer: I didn't put the time in to grok your design, I just tried your Attempt #1 in the playground.

That said, making the trait look like this enabled it to compile:

pub trait InterfaceProvider {
    // This has a lifetime now
    type Interface<'a>: Interface where Self: 'a;
    // We give the lifetime here a name...
    fn with_interface<'t, F>(&'t mut self, func: F) -> io::Result<()>
    where
        // ...and use it here
        F: FnOnce(&mut Self::Interface<'t>) -> io::Result<()>;
        // As otherwise you get a higher-ranked related error that basically
        // amounts to "Self has to be 'static"
}

The downside of naming the lifetime is that you won't be able to use self after creating the Interface<'_>, unless you can deconstruct it and get the &mut Self back out.

1 Like

That works, but ...

  • I don't understand why the explicit 't lifetime is necessary. I thought that this:

    fn with_interface<'t, F>(&'t mut self, func: F) -> io::Result<()>
    where
        F: FnOnce(&mut Self::Interface<'t>) -> io::Result<()>;
    

    was equivalent to:

     fn with_interface<F>(&mut self, func: F) -> io::Result<()>
     where
         F: FnOnce(&mut Self::Interface<'_>) -> io::Result<()>;
    

    but that doesn't compile. What's the difference?

  • Regarding this comment:

    I'm not clear how far-reaching "you won't be able to use self" is. Can I use self within with_interface() after calling func(&mut iface) and after iface is dropped? Is the provider still usable after returning from with_interface()?

  • I tried to adapt StandardInterface for the new traits like so:

    
    #[derive(Clone, Debug, Eq, PartialEq)]
    pub struct StandardInterfaceProvider;
    
    impl InterfaceProvider for StandardInterfaceProvider {
        type Interface<'a> = BasicInterface<'a, io::StdinLock<'static>, io::StdoutLock<'static>>;
    
        fn with_interface<'s, F>(&'s mut self, func: F) -> io::Result<()>
        where
            F: FnOnce(&mut Self::Interface<'s>) -> io::Result<()>,
        {
            let mut stdin = io::stdin().lock();
            let mut stdout = io::stdout().lock();
            let mut iface = BasicInterface::new(&mut stdin, &mut stdout);
            func(&mut iface)
        }
    }
    

    but that failed to compile with:

    error[E0597]: `stdin` does not live long enough
       --> crates/advcore/src/interface.rs:110:45
        |
    104 |     fn with_interface<'s, F>(&'s mut self, func: F) -> io::Result<()>
        |                       -- lifetime `'s` defined here
    ...
    108 |         let mut stdin = io::stdin().lock();
        |             --------- binding `stdin` declared here
    109 |         let mut stdout = io::stdout().lock();
    110 |         let mut iface = BasicInterface::new(&mut stdin, &mut stdout);
        |                                             ^^^^^^^^^^ borrowed value does not live long enough
    111 |         func(&mut iface)
        |         ---------------- argument requires that `stdin` is borrowed for `'s`
    112 |     }
        |     - `stdin` dropped here while still borrowed
    
    error[E0597]: `stdout` does not live long enough
       --> crates/advcore/src/interface.rs:110:57
        |
    104 |     fn with_interface<'s, F>(&'s mut self, func: F) -> io::Result<()>
        |                       -- lifetime `'s` defined here
    ...
    109 |         let mut stdout = io::stdout().lock();
        |             ---------- binding `stdout` declared here
    110 |         let mut iface = BasicInterface::new(&mut stdin, &mut stdout);
        |                                                         ^^^^^^^^^^^ borrowed value does not live long enough
    111 |         func(&mut iface)
        |         ---------------- argument requires that `stdout` is borrowed for `'s`
    112 |     }
        |     - `stdout` dropped here while still borrowed
    
    For more information about this error, try `rustc --explain E0597`.
    

    which I think is complaining that the function locals stdin & stdout don't live as long as the StandardInterfaceProvider instance, which is true, and fixing that would presumably require changing the lifetimes in the trait somehow, but I don't know how.

The latter is equivalent to for<'a, 'b> F: FnOnce(&'a mut Self::Interface<'b>) -> io::Result<()>.

In general, Fn* traits follow the same lifetime elision rules as function declarations. This has two elided, unnamed lifetime parameters that may be freely chosen by the caller:

fn foo(x: &[&str]) {}

// unelided form:
fn foo<'a, 'b>(x: &'a [&'b str]) {}

Therefore, so does this:

where
    F: FnOnce(&[&str]),

    // unelided form:
    for<'a, 'b> F: FnOnce(&'a [&'b str]),

No, that's what using a lifetime parameter of fn with_interface makes impossible — the caller is allowed to specify a long 't and write a func that takes advantage of it. (Difficult in this case, but not impossible.)

Yes — unless the caller purposefully specifies a longer 't when calling with_interface(). (That's on them.)

You can pass a borrow of a local variable to a for<'a> FnOnce(TypeWithBorrows<'a>), because the call site gets to pick a sufficiently short lifetime. You can't pass a borrow of a local variable to a FnOnce(TypeWithBorrows<'some_outer_lifetime_param>), because the variables aren’t guaranteed to (and in fact will never) outlive 'some_outer_lifetime_param.

2 Likes

I don't like that. How do I change that?

I think I understood that, but how do I declare the type parameter in <StandardInterfaceProvider as InterfaceProvider>::Interface?

type Interface<'a> = BasicInterface</* What goes here? */, io::StdinLock<'static>, io::StdoutLock<'static>>;

@kpreid answered this, but I'll point out that the bound you really want is:

F: for<'a, 'b where Self: 'b> FnOnce(&'a mut Self::Interface<'b>) -> io::Result<()>;

But there's no direct way to write that.

(The reason that you got an outlives error is that Interface<'b> is only defined when Self: 'b, so the for<'a, 'b> F: bound -- without an upper limit on 'b -- can't be met.)

You can instead jump through hoops to emulate the bound you really want.

Here's a version of that, without attempting the dyn safety parts:

2 Likes

... I think I'll stick with my original code.

I'm a bit confused, to be honest.

Clear enough, so far.

Sounds like an attempt to port some Java-ish "abstract provider interface factory" into a language where it doesn't quite belong. Why not flatten it out?

Summary
pub trait Interface {
    /// Read a line of input from the interface. Returns `None` on end of input.
    fn get_input(&mut self) -> Result<Option<String>>;
    /// Display the given text in the interface.
    fn show_output(&mut self, text: &str) -> Result;
    /// To ensure that any necessary startup code ran before [Interface::with_self].
    fn perform_startup(&mut self) {}
    /// To ensure that any necessary cleanup code ran after [Interface::with_self].
    fn perform_cleanup(&mut self) {}
    /// Run `f` with the associated `Interface`.
    fn with_self<F>(&mut self, f: F) -> Result
    where F: FnOnce(&mut Self) -> Result { 
        self.perform_startup();
        let result = f(self);
        self.perform_cleanup();
        result 
    }
}

Are you crystal clear about the end result? Because the whole lot of it looks like a completely unnecessary attempt to over-engineer the heck out of it "just in case". What kind of data will your provider contain? What prevents you / your crate's users to provide it in the "interface" itself?

Ignoring the rather confusing distinction in between "standard" and "basic" (which are quite synonymous in my head), this implementation makes no sense: not only you're mixing the "standard" provider with the "basic" interface themselves, but the two are in no way "linked".

Nevertheless, you're still creating your own BasicInterface from scratch which is used exactly once before being discarded, despite the func itself being able to only use it as &mut Interface.

How would a user (or even yourself - a year from now) would have to go about figuring out the most appropriate type Interface<'a> in the first place? Why is it just an 'a and not a clearly defined split in between the reader-specific 'r and the writer-specific 'w? It's just confusing.

Assuming you insist on the split in between the Interface and Provider at least make it:

Summary
pub trait Interface {
    /// Read a line of input from the interface. Returns `None` on end of input.
    fn get_input(&mut self) -> Result<Option<String>>;
    /// Display the given text in the interface.
    fn show_output(&mut self, text: &str) -> Result;
}

pub trait Provider {
    type IO: Interface;
    /// Fetch the [Interface] from the given [InterfaceProvider];
    fn get_interface(&mut self) -> &mut Self::IO;
    /// Run `f` with the associated [Interface].
    fn with_interface<F>(&mut self, f: F) -> Result
    where F: FnOnce(&mut Self::IO) -> Result { 
        // + some start-up logic
        f(self.get_interface()) 
        // + clean-up
    }
}

At this point, there's no ambiguity left with regards to which exact lifetimes of which exact references to which exact Interface and/or Provider are being declared all over.

The problem with "flattening it out" is that there's then nothing stopping the calling code from invoking get_input() & show_output() without the enclosing calls to perform_{startup,cleanup}(). Enforcing proper call order is easy enough with Rust's type system, so why not do so?

Also, for the record, this particular design is inspired by Python's context managers, not by anything from Java. InterfaceProvider was even originally named InterfaceContext before I started overthinking things.