Daniel Porteous
Daniel Porteous

Reputation: 6343

Apply trait bounds to associated type

Let's say I have a trait like this:

trait InternalError {
    fn internal(error: String) -> Self;
}

If I want to use this in a function, I can do so like this:

struct MyU16(pub u16);

fn my_try_from<E: InternalError>(value: u32) -> Result<MyU16, E> {
    if value < u16::MAX as u32 {
        Ok(MyU16(value as u16))
    } else {
        Err(E::internal("invalid".to_string()))
    }
}

This works just fine.

Now imagine instead of using my own function, I want to use TryFrom:

impl<E: InternalError> TryFrom<u32> for MyU16 {
    type Error = E;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        if value < u16::MAX as u32 {
            Ok(MyU16(value as u16))
        } else {
            Err(E::internal("invalid".to_string()))
        }
    }
}

This however does not work. I have tried various different ways to phrase this, but I can't figure out how to say "this TryFrom impl, in the error case, returns something that impls InternalError".

How can I do this properly?

Upvotes: 2

Views: 506

Answers (2)

Markus Klein
Markus Klein

Reputation: 1376

this TryFrom impl, in the error case, returns something that impls InternalError

TryFrom wants to know at compile time what error type is returned. You seem to want to decide on the type at runtime.

Let's assume FirstError and SecondError both implement InternalError.

In this case

impl TryFrom<u32> for MyU16 {
    type Error = FirstError;

    fn try_from(value: u32) -> Result<Self, FirstError> {
        /*...*/
    }
}

Would compile, but of course try_from can only ever return FirstError.

If you want to decide at runtime between FirstError and SecondError however. The associated error type (not trait) needs to be able to hold both.

impl TryFrom<u32> for MyU16 {
    type Error = Box<dyn InternalError>;

    fn try_from(value: u32) -> Result<Self, Box<dyn InternalError>> {
        /*...*/
        Box::new(FirstError::internal("invalid".to_string()))
    }
}

This also gives a hint of why enums are the idiomatic way to represent errors in Rust: Usually you do know at compile time what could go wrong, but you do not yet know what actually does go wrong (if anything). Actual errors occurring you only know at runtime. Therfore it is a good idea to have the type being able to represent a set of potential error, and the instance to be a specific error.

An enumeration fits that bill and makes it clear for the user what could go wrong and in many situations allows you to create the error without an additional heap allocation.

Upvotes: 2

jsstuball
jsstuball

Reputation: 4911

The reason it won't compile is there is a rule that the type parameter in the impl declaration has to appear in the implementing type (impl<T> Foo<T>) or the implemented trait (impl<T> MyTrait<T> for Foo). You can't just write impl<T> for Foo and only mention T again inside the impl block.

As the expanded error suggests, there is a workaround introducing a phantom type to the implementing type, but you might not like how it makes your code look:

use std::marker::PhantomData;

struct MyU16<T>(pub u16, PhantomData<T>);

impl<E: InternalError> TryFrom<u32> for MyU16<E> {
    type Error = E;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        if value < u16::MAX as u32 {
            Ok(MyU16(value as u16, PhantomData))
        } else {
            Err(E::internal("invalid".to_string()))
        }
    }
}

Upvotes: 0

Related Questions