Different error type resolving behavior between for loop and iterator

I have this working code.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("MyError: {0}")]
    MyError(String),
    #[error("Conversion error: {0}")]
    ConversionError(#[from] std::io::Error),
}

// Common types
pub type Result<T> = std::result::Result<T, Error>;

struct StoredEvent {
    id: String,
}

trait ApplicationEvent: TryFrom<StoredEvent> {}

trait Decider {
    type Event: ApplicationEvent;

    fn decide(event: Self::Event) -> Result<()>;
}

// Domain specific
struct BusinessEvent {
    id: String,
}
impl ApplicationEvent for BusinessEvent {}
impl TryFrom<StoredEvent> for BusinessEvent {
    type Error = std::io::Error;

    fn try_from(stored_event: StoredEvent) -> std::result::Result<Self, Self::Error> {
        Ok(Self {
            id: stored_event.id.clone(),
        })
    }
}

struct BusinessDecider;
impl Decider for BusinessDecider {
    type Event = BusinessEvent;

    fn decide(_event: Self::Event) -> Result<()> {
        Ok(())
    }
}

struct Processor<D: Decider> {
    decider: D,
}

impl<D: Decider> Processor<D>
where
    Error: From<<<D as Decider>::Event as TryFrom<StoredEvent>>::Error>,
{
    fn with(decider: D) -> Self {
        Self { decider }
    }

    fn process_all(&self) -> Result<Vec<D::Event>> {
        let stored_events = vec![
            StoredEvent { id: "1".to_owned() },
            StoredEvent { id: "2".to_owned() },
            StoredEvent { id: "3".to_owned() },
            StoredEvent { id: "4".to_owned() },
        ];

        let mut result = vec![];
        for obj in stored_events {
            let converted: D::Event = obj.try_into()?;
            result.push(converted);
        }

        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    fn multiple_works() {
        let decider = BusinessDecider {};
        let processor = Processor::with(decider);
        let event = processor.process_all().unwrap();
        assert_eq!("1", event[0].id);
        assert_eq!("2", event[1].id);
        assert_eq!("3", event[2].id);
        assert_eq!("4", event[3].id);
    }
}

The interesting bits are the where clause

where
    Error: From<<<D as Decider>::Event as TryFrom<StoredEvent>>::Error>,

and the for loop

let mut result = vec![];
for obj in stored_events {
    let converted: D::Event = obj.try_into()?;
    result.push(converted);
}

Without the where clause and the From<io::Error> generated by the thiserror error conversion, I get this compiler error:

error[E0271]: type mismatch resolving `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error == Error`
  --> src/lib.rs:72:43
   |
72 |             let converted: D::Event = obj.try_into()?;
   |                                           ^^^^^^^^ expected associated type, found enum `Error`
   |
   = note: expected associated type `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error`
                         found enum `Error`
   = help: consider constraining the associated type `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error` to `Error` or calling a method that returns `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

The compiler thankfully came up with the where clause after attempting to fix the compiler error by implementing the error conversion.

I then wanted to improve on the for loop with into_iter on the collection like so:

let result = stored_events
    .into_iter()
    .map(|event| event.try_into())
    .collect::<Result<Vec<D::Event>>>()?;

but I still get the above error even with the left-in error conversion and where clause:

error[E0271]: type mismatch resolving `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error == Error`
  --> src/lib.rs:79:32
   |
79 |             .map(|event| event.try_into())
   |                                ^^^^^^^^ expected associated type, found enum `Error`
   |
   = note: expected associated type `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error`
                         found enum `Error`
   = help: consider constraining the associated type `<<D as Decider>::Event as TryFrom<StoredEvent>>::Error` to `Error`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

My questions are two-fold:

  1. Why does the current solution work for the for loop but not into_iter?
  2. Is there a better way to specify the Error-TryFrom<StoredEvent>>::Error constraint "earlier" in the trait/type hierarchy?

Because the for loop uses the question mark operator on the error, and the question mark operator inserts a conversion step for the error type. The collect solution does not do this.

Note that the question mark operator on the collect call is not sufficient because the error type is already required to be thiserror::Error when it reaches the question mark by the output type you specified on collect.

You can allow the Result returned by collect to have any error type to make it work. In that case, the question mark will do the conversion.

let result = stored_events
    .into_iter()
    .map(|event| event.try_into())
    .collect::<std::result::Result<Vec<D::Event>, _>>()?;

You could also put the error conversion in the map call.

let result = stored_events
    .into_iter()
    .map(|event| event.try_into().map_err(|e| e.into()))
    .collect::<Result<Vec<D::Event>>>()?;
1 Like

Wonderful explanation. Thank you!