How to call the same method on two objects of different types?

My project dependencies:

// Cargo.toml
...
[dependencies]
md-5 = "0.10.6"
sha1 = "0.10.6"

I wish to create a function that takes a specified hash algorithm, and generates the corresponding digest.

The way to choose an algorithm is by hash enumeration.

// foo.rs
use md5::{Md5, Digest};
use sha1::Sha1;


pub enum Hash {
    MD5,
    SHA1
}

pub fn foo(content: &str, hash: Hash) -> String {
    let mut hasher = match hash {
        Hash::MD5 => Md5::new(),
        Hash::SHA1 => Sha1::new(),  // Error
    };

    hasher.update(content);
    format!("{:x}", hasher.finalize_reset())
}
// main.rs
mod foo;

use foo::{foo, Hash};

fn main() {
    let c = "Hello, World!";
    let d = foo(c, Hash::MD5);
    println!("{}", d);
}

When I try to run main.rs, the error occurred:

error[E0308]: `match` arms have incompatible types
  --> src\foo.rs:13:23
   |
11 |       let mut hasher = match hash {
   |  ______________________-
12 | |         Hash::MD5 => Md5::new(),
   | |                      ---------- this is found to be of type `CoreWrapper<Md5Core>`
13 | |         Hash::SHA1 => Sha1::new(),
   | |                       ^^^^^^^^^^^ expected `CoreWrapper<Md5Core>`, found `CoreWrapper<Sha1Core>`
14 | |         // Hash::SHA1 => Md5::new(),
15 | |     };
   | |_____- `match` arms have incompatible types
   |
   = note: expected struct `CoreWrapper<Md5Core>`
              found struct `CoreWrapper<Sha1Core>`

For more information about this error, try `rustc --explain E0308`.

Clearly, I specified two different types of values for variable hasher, it caused an error.

But in Python, Everything is Object and Dynamic. I wish to call hasher succinctly like that, because CoreWrapper and CoreWrapper all have the same method.

I'd be better grateful if anyone is willing to give me an idea to achieve that.

One way to accomplish that is by not trying to store the hasher in a local in such a way that the types don't match. Instead just hash it directly within each match arm:

pub fn foo(content: &str, hash: Hash) -> String {
    match hash {
        Hash::MD5 => {
            let mut hasher = Md5::new();
            hasher.update(content);
            format!("{:x}", hasher.finalize_reset()) 
        },
        Hash::SHA1 => {
            let mut hasher = Sha1::new();
            hasher.update(content);
            format!("{:x}", hasher.finalize_reset())
        },
    }
}
2 Likes

Another way is to create a trait covering the shared functionality, implement it for the desired types, and coerce the different types to Box<dyn Trait> ("type erase" them).

3 Likes

Looking at the docs, it seems that both may already implement digest::DynDigest.

2 Likes

Ultimately that's probably the more flexible way to go.

The only reason I made the suggestion I did is because the Hamming distance from OP's example to mine is about as low as it could reasonably go, i.e. it's less work to get done.

But especially if there's already a dyn-safe trait that all hashers implement, that's probably the better way to go in terms of extensibility and perhaps even maintenance.

1 Like

For your specific code example you could also write the following:

pub fn foo(content: &str, hash: Hash) -> String {
    match hash {
        Hash::MD5 => format!("{:x}", Md5::digest(content)),
        Hash::SHA1 => format!("{:x}", Sha1::digest(content)),
    }
}

Another alternative (especially if you need to do more than just formatting) would be writing a generic function that you call with a different generic argument based on the match branch you're in:

pub fn foo(content: &str, hash: Hash) -> String {
    match hash {
        Hash::MD5 => foo_generic::<Md5>(content),
        Hash::SHA1 => foo_generic::<Sha1>(content),
    }
}

fn foo_generic<T: Digest>(content: &str) -> String {
    format!("{:x}", T::digest(content))
}
3 Likes

Thank you so much for reply.

However, it's important to note that this is only a minimal reproduction. In my real code, if we were to adopt the proposed solution, we would have to implement quite similar logic within every arm of the match. It might be possible that this isn't the most ideal solution under the circumstance

This is actual code:

use strum::{Display, EnumIter, FromRepr};
use md5::{Md5, Digest};
use sha1::Sha1;

use std::path::PathBuf;
use std::{fs, io};
// use std::ffi::OsStr;


// Supported (hash) algorithms
#[derive(Display, EnumIter, FromRepr)]
pub enum Algorithms {
    #[strum(to_string="MD5")]
    MD5,
    #[strum(to_string="SHA1")]
    SHA1
    // TODO Add more hash algorithms as needed
}

pub struct Fileck {
    target: PathBuf,
    algorithm: Algorithms,
    ignore_files: Vec<PathBuf>,
}

impl Fileck {
    pub fn new<TargetPath, IgnoreFile>
        (target: TargetPath, algorithm: Algorithms, ignore_files: Vec<IgnoreFile>) -> Self
    where 
        TargetPath: Into<PathBuf>, 
        IgnoreFile: Into<PathBuf>,
    {
        let ignore_files = ignore_files
            .into_iter()
            .map(|f| f.into()).collect();
        
        Fileck {
            target: target.into(),
            algorithm,
            ignore_files,
        }
    }

    // TODO
    pub fn get_algorithms_list(&self) -> Vec<Algorithms> {
        todo!();
    }

    pub fn gen_checklist(&self) -> Result<String, io::Error> {
        let mut hasher = match self.algorithm {
            // Hash::MD5 => Md5::new(),  // Error
            Algorithms::MD5 => Sha1::new(),  // FIXME Replace SHA1 with MD5    
            Algorithms::SHA1 => Sha1::new(),
        };
        let mut checklist_content = String::new();

        for entry in fs::read_dir(&self.target)? {
            // XXX Extract the logic that handels a single file into a function 
            let entry = entry?;
            let obj_in_target = entry.path();

            // Skip if object is not a file (folder)
            if ! obj_in_target.is_file() {
                continue;
            }
            let file_in_target = obj_in_target;

            // Skip ignored files
            if self.ignore_files.contains(&file_in_target) {
                continue;
            }

            // Read the name and content of file and Calculate hash digest

            // HACK Unsafe code here
            // Handle None value when failed to get filename
            let filename = file_in_target.file_name().unwrap().to_str().unwrap();
            // let filename = file_in_target.file_name()
            //     .unwrap_or_else(|| OsStr::new("FaildToGetFilename"))
            //     .to_str().unwrap();
            let content = fs::read(&file_in_target)?;
            hasher.update(&content);
            let hash = format!("{:x}", hasher.finalize_reset());

            checklist_content.push_str(&format!("{};{}\n", filename, hash));
        }

        // Write to checklist file
        fs::write(self.target.join("checklist.txt"), &checklist_content)?;

        Ok("Generated checklist successfully!".to_string())
    }
}

Thank you so much for reply.

Although I know nothing about Box, I tried to refactor my code using this way:

use digest::{Digest, DynDigest};
use md5::Md5;
use sha1::Sha1;


pub enum Hash {
    MD5,
    SHA1
}

pub fn foo(content: &str, hash: Hash) -> String {
    let mut hasher = match hash {
        Hash::MD5 => Box::new(Md5::new()) as Box<dyn DynDigest>,
        Hash::SHA1 => Box::new(Sha1::new()) as Box<dyn DynDigest>,
    };

    hasher.update(content);
    format!("{:x}", hasher.finalize_reset())
}

My dependencies:

// cargo.toml
...
[dependencies]
digest = "0.10.7"
md-5 = "0.10.6"
sha1 = "0.10.6"

An error occurred:

error[E0308]: mismatched types
   --> src\trying.rs:17:19
    |
17  |     hasher.update(content);
    |            ------ ^^^^^^^ expected `&[u8]`, found `&str`
    |            |
    |            arguments to this method are incorrect
    |
    = note: expected reference `&[u8]`
               found reference `&str`
note: method defined here
   --> ...\digest-0.10.7\src\digest.rs:136:8
    |
136 |     fn update(&mut self, data: &[u8]);
    |        ^^^^^^

error[E0277]: the trait bound `Box<[u8]>: LowerHex` is not satisfied
   --> src\trying.rs:18:21
    |
18  |     format!("{:x}", hasher.finalize_reset())
    |              ----   ^^^^^^^^^^^^^^^^^^^^^^^ the trait `LowerHex` is not implemented for `Box<[u8]>`
    |              |
    |              required by a bound introduced by this call
    |
    = help: the following other types implement trait `LowerHex`:
              &T
              &mut T
              GenericArray<u8, T>
              Saturating<T>
              Wrapping<T>
              crossterm::event::KeyEventState
              crossterm::event::KeyModifiers
              crossterm::event::KeyboardEnhancementFlags
            and 21 others
note: required by a bound in `core::fmt::rt::Argument::<'a>::new_lower_hex`
   --> ...\lib/rustlib/src/rust\library\core\src\fmt\rt.rs:124:33       
    |
124 |     pub fn new_lower_hex<'b, T: LowerHex>(x: &'b T) -> Argument<'_> {
    |                                 ^^^^^^^^ required by this bound in `Argument::<'a>::new_lower_hex`
    = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)

It seems that the parameter type has been changed

That made me confused again..

The non-dyn version of the trait has the update method defined as:

fn update(&mut self, data: impl AsRef<[u8]>);

This is a generic function that can take any type which implements AsRef<[u8]> (i.e. anything that can be viewed as a byte array, which includes str).

However, trait objects can't have generic methods, and so DynDigest's version of update is instead just defined as:

fn update(&mut self, data: &[u8]);

So it needs to be specifically passed a &[u8]. You can use content.as_bytes() when you call it to explicitly get a &[u8] from your &str.

4 Likes

Thank you so much for reply.

I'm sorry, I forgot that I was solving with smart pointer.

I rewrite my code like this:

use digest::{Digest, DynDigest};
use md5::Md5;
use sha1::Sha1;
use hex;


pub enum Hash {
    MD5,
    SHA1
}


pub fn foo(content: &str, hash: Hash) -> String {
    let mut hasher = match hash {
        Hash::MD5 => Box::new(Md5::new()) as Box<dyn DynDigest>,
        Hash::SHA1 => Box::new(Sha1::new()) as Box<dyn DynDigest>,
    };

    hasher.update(content.as_bytes());
    let hash_digest = hasher.finalize_reset();
    hex::encode(hash_digest)
}

For convenience, I used crate hex, hex:: encode accepts a byte slice (Box<[u8]>can dereference automatically as it) and returns a hexadecimal string representing the byte content

We are writing test cases, let's see if it works:

// --- snip ---
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_case_01() {
        let s = "Hello World!";
        assert_eq!(foo(s, Hash::MD5), "ed076287532e86365e841e92bfc50d8c");
        assert_eq!(foo(s, Hash::SHA1), "2ef7bde608ce5404e97d5f042f95f89f1c232871");
    }
    
    #[test]
    fn test_case_02() {
        let s = "Have fun with Rust :)";
        assert_eq!(foo(s, Hash::MD5), "2b984c8a534efb23e338e55472f69ca8");
        assert_eq!(foo(s, Hash::SHA1), "8ff1fb4254f5c0e78b85bf0acf69420eaa0f5108");
    }
}

Output:

Green - What a comforting color :slight_smile:

Looking back, is it really worth doing this? Anyway, we ultimately achieved it

I swear I will definitely learn about traits, smart pointers, and something else

Have fun with Rust :slight_smile: