A question about my first crate


#1

Hello,

While I’ve been an Android dev for some time, I’m been keeping an eye on Rust and am really impressed with the community and how far the language has evolved. :slight_smile:

I wanted to practice creating my own crate and putting it into crates.io, so I created a super-simple project here: https://github.com/Digipom/decibel

This is a straight port of similar utility code I have in my Android app:

	public static double amplitudeToDbfs(double amplitude) {
		return Math.log10(amplitude) * 20.0;
	}

	public static double dbfsToAmplitude(double dbfs) {
		return Math.pow(10, dbfs / 20.0);
	}

This is what I ended up with in Rust:

pub struct AmplitudeRatio<T: Copy>(pub T);
pub struct DecibelRatio<T: Copy>(pub T);

impl From<DecibelRatio<f32>> for AmplitudeRatio<f32> {
    #[inline]
    fn from(decibels: DecibelRatio<f32>) -> AmplitudeRatio<f32> {
        AmplitudeRatio(f32::powf(10.0, decibels.decibel_value() / 20.0))
    }
}

impl From<AmplitudeRatio<f32>> for DecibelRatio<f32> {
    #[inline]
    fn from(amplitude: AmplitudeRatio<f32>) -> DecibelRatio<f32> {
        DecibelRatio(f32::log10(amplitude.amplitude_value()) * 20.0)
    }
}

impl<T: Copy> AmplitudeRatio<T> {
    #[inline]
    pub fn amplitude_value(&self) -> T {
        self.0
    }
}

impl<T: Copy> DecibelRatio<T> {
    #[inline]
    pub fn decibel_value(&self) -> T {
        self.0
    }
}

This is somewhat longer than the Java version, as I wanted to take advantage of Rust’s “newtypes” to add some safety in situations where I’d use a decibel when I meant to use an amplitude, or vice-versa. Using the newtypes makes this more explicit.

Ideally this could be made more generic but right now I don’t see a way to do it without hardcoding the types, as Rust doesn’t support duck typing and I didn’t see a “powf” or “log10” trait so I’d have to create my own.

I wanted to create this crate mainly as a learning experience to get used to the whole process, and to start forcing myself to learn more Rust as I can definitely see a bright future for it and lots of advantages to using Rust for infrastructure projects. I’d be happy to hear some criticisms to see how I can do better, in the interest of learning! :slight_smile:


#2

Hi there! Well done publishing your first crate. I’ve read through the code and it looks pretty good. I don’t know much about decibels, but I do know that they’re special and shouldn’t be treated like ordinary numbers when adding and subtracting - a perfect use for the newtype pattern that you’ve discovered.

Also, you didn’t actually ask a question :slight_smile: but you say you want to make your code more generic. You’re right that the Rust standard library doesn’t have a trait for powers and logarithms. It did used to have one, though, but the Float trait was moved into the num crate. Here are the num docs, and there are trait methods for both powf and log10 implemented for both f32 and f64.

Using this, you could replace T: Copy with T: Float, and f32::log10 and the rest with Float::log10. Ask here if you run into any problems.

Another suggestion I have for your crate is that you could derive some definitions for AmplitudeRatio and DecimalRatio: they could be PartialEq, Copy, Clone, and Debug and possibly others at no cost.


#3

Thanks, this is very helpful! I am running into one issue with this though; if I try to implement my from method like this:

impl From<AmplitudeRatio<Float>> for DecibelRatio<Float> {
    #[inline]
    fn from(amplitude: AmplitudeRatio<Float>) -> DecibelRatio<Float> {
        DecibelRatio(Float::log10(amplitude.amplitude_value()) * 20.0)
    }
}

I get the following error: the trait 'num::traits::Float' cannot be made into an object [E0038]. The decibel_value fn and the other seem OK with the Float bounds; it’s just the from that’s giving me problems.

I can work around it by making two impls with f32 and f64, but that was the duplication I was hoping to avoid. :slight_smile: I’m not sure how else to work around it at the moment.

Edit:

I can get it working like this:

impl<T: Float> From<AmplitudeRatio<T>> for DecibelRatio<T> {
    #[inline]
    fn from(amplitude: AmplitudeRatio<T>) -> DecibelRatio<T> {
        DecibelRatio(Float::log10(amplitude.amplitude_value()) * NumCast::from(20.0).unwrap())
    }
}

Not a fan of the cast + unwrapping, though. It seems to be moving something that should be checked at compile time to runtime. Maybe this is a place where a macro might work better?


#4

I wrote half a post and then you edited it with the exact same solution I was going to post! So it looks like you got it.

It’s actually the same problem that confused me when I came to Rust from Java-land, where you can use Float to mean “Either a float or a sub-type of Float”. Rust insists that you be more specific with T: Float, meaning “A sub-type of Float, but the same sub-type every time T is used”, if that makes sense.

I agree. In one of my crates, I got around this using my own trait but hopefully one day it’ll be a compile-time check.


#5

Indeed, all of my Java experience isn’t helping me out all that much here. :wink: After thinking about the error some, I realized that Rust thought that I was trying to create an “abstract” type instead of monomorphizing the code.

I think your approach is the way to go, either that or using a macro but your way would certainly be easier to understand.

Thanks so much for the help! :slight_smile:


#6

No prob!

Right again. It is possible to write a function that takes an abstract trait type as input, such as &Trait or Box<Trait>. That doesn’t work with Float which is why the compiler tells you off, but you have it working so you don’t need to worry about all that!