Returning a &dyn trait, two seemingly identical code blocks but only one works

Hey, hoping someone can help me out. I have reduced my issue to the smallest possible code:

use std::collections::HashMap;
use std::fmt::Debug;

trait Walkable {
    fn walk(&self, segment: &str) -> Result<&dyn Walkable, String>;
}

impl<I: Walkable> Walkable for HashMap<&str, I> {
    fn walk(&self, segment: &str) -> Result<&dyn Walkable, String> {
        // Does not work:
        // self.get(segment).ok_or("failed".to_string())

        // Works:
        match self.get(segment) {
            Some(v) => Ok(v),
            None => Err("failed".to_string()),
        }
    }
}

So the general idea is I'm trying to create a trait to handle generically walking down a tree. So like:
context = {"cat": {"food": {"good": "meat", "bad": "pizza"}}}
context.walk("cat").walk("food") would return {"good": "meat", "bad": "pizza"}

So I'm trying to create a generic implementation of this Walkable for Hashmap. My problem comes when I'm trying to use ok_or. As you can see in the code block above, when I do the match statement manually, everything works fine. But if I comment out the match statement and uncomment the ok_or line it does not work. The thing is, I've looking through the implementation for ok_or() and I can't seem the find the difference between the two. My reasoning is:

self is a HashMap<&str, I>
I is a Walkable
Therefor self is a HashMap<&str, Walkable>

HashMap::get returns a Option<&V> so therefore an Option<&Walkable>

ok_or is implemented as:

pub fn ok_or<E>(self, err: E) -> Result<T, E> {
    match self {
        Some(v) => Ok(v),
        None => Err(err),
    }
}

Which appears to be identical to my working match statement. Filling in the types of &Walkable from the Option and String for the error we'd get:

pub fn ok_or<E>(self, err: String) -> Result<&Walkable, String> {
    match self {
        Some(v: &Walkable) => Ok(v),
        None => Err(err),
    }
}

Here lies the fault in the logic: I is some concrete type that implements the Walkable trait: I != dyn Walkable.

        // Does not work:
        // self.get(segment) // : Option<&I>
        //     .ok_or("failed".to_string()) // T = &I, E = String -> Result<&I, String>
        //     (coercion inside a Result is not possible)

        // Works:
        match self.get(segment) {
            Some(v/* : &I */) => Ok(v), // coercion site from `&I` to `&dyn Walkable`
            None => Err("failed".to_string()),
        }

So you need to chain something like .map(|v| v as _) to ok_or(...) to enable a coercion site from &I to &dyn Walkable

3 Likes

Since the first version doesn't have a place where the compiler will coerce implicitly, you could coerce the types explicitly like this:

self.get(segment).map(|x| x as &dyn Walkable).ok_or("failed".to_string())

or even this:

self.get(segment).map(|x| x as _).ok_or("failed".to_string())

Personally, I might write the function like this:

fn walk(&self, segment: &str) -> Result<&dyn Walkable, String> {
    let walkable = self.get(segment).ok_or("failed".to_string())?;
    Ok(walkable)
}
3 Likes

Awesome, thank you both! That worked. Seems I can only mark 1 as the solution so I marked the first reply but I appreciate both your responses.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.