Should I write code with From and Into traits?

Can you explain which code is better and WHY? (It's just an example)

  1. simple.rs
#![allow(dead_code)]

use std::error::Error;

pub struct App {
    pub port: u16,
}

pub struct Database {
    pub url: String,
}

pub struct Config {
    pub app_port: u16,
    pub db_url: String,
}

impl Config {
    pub fn build() -> Result<Self, Box<dyn Error>> {
        let app_port: u16 = 8080;
        let db_url: String = "connection".to_string();
        let config = Self { app_port, db_url };

        Ok(config)
    }
}

impl App {
    pub fn new(port: u16) -> Self {
        Self { port }
    }
}

impl Database {
    pub fn new(url: String) -> Self {
        Self { url }
    }
}
  1. with_traits.rs (with From and Into traits)
#![allow(dead_code)]

use std::{error::Error, sync::Arc};

pub struct Config {
    pub app_port: u16,
    pub db_url: String,
}

pub struct App {
    pub port: u16,
}

pub struct Database {
    pub url: String,
}

impl Config {
    pub fn build() -> Result<Arc<Self>, Box<dyn Error>> {
        let app_port: u16 = 8080;
        let db_url = "connection".to_string();
        let config = Self { app_port, db_url };

        Ok(Arc::new(config))
    }
}

impl<T> From<T> for App
where
    T: AsRef<Config>,
{
    fn from(value: T) -> Self {
        let port = value.as_ref().app_port;

        Self { port }
    }
}

impl<T> From<T> for Database
where
    T: AsRef<Config>,
{
    fn from(value: T) -> Self {
        let url = value.as_ref().db_url.clone();

        Self { url }
    }
}
  1. main.rs
mod simple;
mod with_traits;

fn main() {
    // version 1
    let config = match simple::Config::build() {
        Ok(config) => config,
        Err(error) => {
            println!("Config init: {error}");
            return;
        }
    };

    let _app = simple::App::new(config.app_port);
    let _database = simple::Database::new(config.db_url);

    // version 2
    let config = match with_traits::Config::build() {
        Ok(config) => config,
        Err(error) => {
            println!("Config init: {error}");
            return;
        }
    };

    let _app: with_traits::App = config.clone().into();
    let _database: with_traits::Database = config.clone().into();
}

Since 'better' isn’t an objective criterion, my answer is inherently subjective and reflects only my personal opinion:

Writing source code is about communicating a story to the human reader. The purpose of From and Into traits is to signal to the reader that a value of one type can (easily) be represented in another type. For example, a smaller integer can naturally be represented as a larger integer type.

Now, ask yourself: For an ordinary human reading the code, does it make sense to say that a Config is an App? Or that a Config is a Database?

In most cases, simplicity wins...

:wink:

2 Likes

From and Into are meant for infallible conversions. That being said, I would expect something named App to eventually have fallibility on its construction, which means that your implementations would have a rather short lifetime (No pun intended).

There's also the argument of intention of the code. The aforementioned traits are meant for conversions between in-memory representations, not for building types. I wouldn't even use the fallible version of them (i.e. TryFrom) for this, for example.

And finally there's the argument of simplicity that mroth has already mentioned.

1 Like