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

⚓ rust    📅 2025-06-09    👤 surdeus    👁️ 2      

surdeus

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 (click for more details)

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 (click for more details)

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

1 post - 1 participant

Read full topic

🏷️ rust_feed