Enum of variants, each with type T, getting unused parameters error

The compiler is giving me error[E0392]: parameter T is never used for something like the following:

use num_traits::Float;

enum ExampleEnum<T: Float = f64> {
    OptionOne(OptionOne<T>),  // Sure seems like T is used here
    OptionTwo(OptionTwo<T>),  // And here too...
}

struct OptionOne<T: Float = f64> { /// stuff }
struct OptionTwo<T: Float = f64> { /// other stuff }

How is T not used? Am I missing something?

This doesn't trigger the warning. Can you post actual code that triggers it?

use num_traits::Float;

enum ExampleEnum<T: Float = f64> {
    OptionOne(OptionOne<T>),
    OptionTwo(OptionTwo<T>),
}

struct OptionOne<T: Float = f64> {
    t: T,
}
struct OptionTwo<T: Float = f64> {
    t: T,
}

I think something just clicked... Try:

use num_traits::Float;

enum ExampleEnum<T: Float = f64> {
    OptionOne(OptionOne<T>),
    OptionTwo(OptionTwo<T>),
}

struct OptionOne<T: Float = f64> { }
impl OptionOne<f64> {
    pub const CONSTANT: f64 = 1.5;
}
struct OptionTwo<T: Float = f64> { }
impl OptionTwo<f64> {
    pub const CONSTANT: f64 = 1.0;
}

Seems like it might be correctly pointing out that T is unused within OptionOne and OptionTwo, but calling it out at the level of ExampleEnum. Oh, and it calls it out for the two options as well, if I'd just bothered to read on...

Is marker::PhantomData the best option here?

It really depends on what you're actually doing and why OptionOne and OptionTwo are generic over T. There are some cases where PhantomData is the right choice, of course -- that's the whole reason it exists. But sometimes people add PhantomData<T> just to shut up the compiler without actually thinking about what it means, and they get themselves more confused down the road. Without more information about what you're hoping to accomplish, there's not enough in your post to guess what the intended semantics are.

If you do try to use PhantomData, note that there are multiple ways to do that:

  • PhantomData<T> is covariant in T and affects drop checking
  • PhantomData<*const T> or PhantomData<fn() -> T> is covariant in T but does not affect drop checking
  • PhantomData<fn(T)> is contravariant in T
  • PhantomData<*mut T> or PhantomData<fn(T) -> T> is invariant in T

This stuff can get kind of complex, but If your type has a legitimate reason to use PhantomData, there's probably only one kind of variance that makes sense. If variance doesn't really apply to your type, then in my experience it's most likely that the generic type is misplaced and using PhantomData is probably just compounding that mistake. But again, it depends on what you're trying to achieve with T.

I'm trying to set a crate up with the following in the lib.rs file:

#[cfg(target_pointer_width = "32")] pub type FloatType = f32;
#[cfg(target_pointer_width = "64")] pub type FloatType = f64;

So that everywhere can be written to take a generic floating point number via:

use crate::FloatType;

pub enum ExampleEnum<T: Float = FloatType> {
   /// Options,
}

Not sure if there's a better way... Maybe just using the selected FloatType directly?

use crate::FloatType;

pub enum ExampleEnum<FloatType> {
    /// Options,
}

But that just felt more restricted.

What does that actually mean for ExampleEnum? You're kind of hand waving the most important part, which is why OptionOne, OptionTwo and ExampleEnum are generic at all. If they don't contain, produce or accept a value of type T, then what's the difference between ExampleEnum<f64> and ExampleEnum<f32>? Why not just have ExampleEnum?

Yes, I still feel like I'm searching for the best way to design a crate to be flexible to target any desired float type, without having to write an f64 / f32 / f128 / f16b / etc. version.

The structs will be full of constants of the desired type. I want the structs to be of, contain only, and produce only that type. So it feels counter-intuitive if this would be the correct form:

struct Metric {}
impl Metric<FloatType> {
    const G: FloatType = 9.80665,
}

But if that's correct, then the enum for selection of unit system is:

enum BaseConstants {
    Metric(Metric),
    Imperial(Imperial),
}

Again, it feels counter-intuitive to NOT use BaseConstants<FloatType>. But then, the final piece is:

pub(crate) struct Const<'a, T: Float = FloatType> {
    g: &'a T
    base: BaseConstants,
    _g: RwLock<Option<T>>
}

Where the field g can be either a ref to the base value, or to an optional override value put in _g.

Perhaps it would be better to have all the constants in the root of the constants.rs file. Then I could just populate the _g fields with the desired value on creation, rather than trying to link in an enum. That would reduce the memory footprint, not having to carry around a const and an alternate option.

Well, now I'm thinking this might do everything I need, while keeping the implementation as simple as possible:

use num_traits::Float;
use crate::FloatType;

const METRIC_G: FloatType = 9.80665;
const METRIC_RHO: FloatType = 1026.021;

pub(crate) struct Const<'a, T: Float = FloatType> {
    _g: RwLock<T>,
    _rho: RwLock<T>,
    pub g: &'a T,
    pub rho: &'a T,
}

impl<'a> Const<'a, FloatType> {
    fn new_metric() -> Self {
        let _g: RwLock<FloatType> = RwLock::new(METRIC_G);
        let _rho: RwLock<FloatType> = RwLock::new(METRIC_RHO);
        Const {
            _g,
            _rho,
            g: &'a *_g.read().unwrap(),
            rho: &'a *_rho.read().unwrap(),
        }
    }
}

impl<'a> Const<'a, FloatType> {
    pub fn set_g(&mut self, g: FloatType) {
        self.g = &'a 0.0;   // Clear the ref to allowing writing to the `RwLock`
        {   // Will this ensure the write gets dropped before the read occurs again?
            let write = self._g.try_write().unwrap();
            *write = g;   // Write the new value
        };
        self.g = &'a *self._g.read().unwrap();   // Then set the reference back
    }
}

Result: No empty structs or enums. Memory footprint is minimized. Getter functions avoided. It still feels somewhat clunky, but it does keep the interface to access values like Const.g simple.

Your set_g method takes &mut Self, so nothing else can be holding any of the RwLocks when it’s called anyway. If you don’t need to allow concurrent modification of the individual constants, you can dispense with the locks entirely:

use num_traits::Float;
use crate::FloatType;

const METRIC_G: FloatType = 9.80665;
const METRIC_RHO: FloatType = 1026.021;

#[derive(Clone,Debug)]
pub(crate) struct Const {
    pub g: FloatType,
    pub rho: FloatType,
}

impl Const {
    fn metric() -> &’static Self {
        static METRIC = Const {
            g: METRIC_G,
            rho: METRIC_RHO,
        };
        &METRIC
    }
}

impl Const {
    pub fn set_g(&mut self, g: FloatType) {
        self.g = g;  // Not really necessary, if g is public anyway
   }
}

Then, you can do something like this:

let params = Const::metric().clone();
params.g = 0.0;
1 Like

Well that is a lot simpler!

Have I led myself in the wrong direction by assuming I should put RwLocks around each value with the aim of being the best form for use sharing among parallel threads? And, similarly, are the Arcs I'm wrapping things in at the top level unnecessary if I can just derive Clone to get what I need?

I know the primitives are meant to be Copy and Clone by default. Does that mean that I can just ignore any questions of setting up for multiple parallel readers?

It’s all performance tradeoffs, but it’s usually best to keep your concurrency code and computation code independent of each other if possible— instead of storing RwLocks inside the structure, it is often cleaner to have a single RwLock that protects the entire thing, and then you can avoid its overhead when you’re not trying to deal with concurrency.

If you’re not mutating the contents, Arc vs clone is purely a performance consideration: is the cost of copying and then destroying the structure more or less than the cost of the atomic increment/decrement? Only profiling can tell you for sure, but small things favor Copy/Clone and big ones favor Arc.

Not exactly; the compiler will complain if it can’t prove certain things, but you still have to understand what’s going on to write the correct code in the first place.

If your code compiles (and doesn’t explicitly use unsafe), it doesn’t ever change a value while another piece of code is reading it. RefCell and RwLock push those checks from compile-time to runtime, which lets you do things that the compiler can’t figure out on its own.

I really appreciate the help understanding this!

The concept I'm aiming for is to have the following setup:

  • An environment struct
    • Which has a field of a single defined set of constants
    • And with some Vecs of objects contained in the environment
  • Each object added to the environment would then get a clone of or reference to the environment's constants
    • Some objects are composites of multiple structs, to categorize/separate out functionality into units.
    • Then each sub-struct would also get a clone of the constants (or would a reference to the constants in the environment be better?).

The environment (when not running) can be set up, constants changed from default, objects added, etc. And then when running the goal is to have fastest read-only access to constants, knowing they will not be changed (at least by design of run/solve functions, or perhaps setting a bool flag to lock everything at the start of a run).

I'm wary of the constants not being editable if all of the objects in the environment have read access to the RwLock holding them, thus the RwLock on the constant fields and an Arc on the field in the environment.

I would probably not give the objects direct access to the final variables until setup is complete and they will no longer be changing. Until the simulation starts, all access should go through an & or &mut Environment. Then, as the last step before starting, the Environment can make an Arc<Consts> and give a clone of it to all of the objects.

You might even want to have a separate EnvironmentBuilder struct that’s fully editable for the setup phase, and a separate Environment that’s optimized for running the simulation. When it’s time to start, you call a function that consumes the EnvironmentBuilder and produces an Environment.

Note that if you already have a &'static Const, then you don't need to clone it or put it in an Arc to use it in a multi-threaded context because &'static Const is Send + Copy + 'static already. Arc<T> is not generally useful for statics because they can never be destroyed anyway, so you can use &'static T instead.

Ahh, so anything beyond

#[derive(Copy, Clone)]
pub(crate) struct Const<T: 'static + Copy + Clone + Send + Float = FloatType> {
    g: T,
    rho: T,
}

is just adding complexity without actually enhancing the functionality?

There are some unusual use cases that the more complex constructions allow, but designing your code to not need them is usually less complex than having to deal with all of the extra stuff in the common cases.

Also, Send is auto-derived when possible, so you don't need to include it in your #[derive(...)].

1 Like

I'm still unclear on exactly what functionality you're trying to achieve.

If the goal is a crate that, once compiled, supports exactly one float type, but that type might be f32 or f64 according to compile time options: none of these types need to be generic over T at all. Just use FloatType. There's no need to bound anything on num_traits::Float, or Send or Copy or whatever, because FloatType is known to be all of those things in advance of creating anything that uses it. There's not even anywhere to put the bounds, because nothing is generic: it all uses FloatType, which happens to be an alias to something of which the exact identity doesn't matter when you're writing the code.

If, on the other hand, the goal is a crate that simultaneously supports multiple float types, i.e. it makes sense to use Const<f32> and Const<f64> in the same program, then the generics are warranted. But that, IMO, throws some doubt on having T default to FloatType -- it might make more sense to have no default and let the user specify T. The majority of your code will be written to deal with generic T anyway. And there's still not really any need to bound T with Copy, Clone, or 'static, except in the functions that actually implement the behavior that depends on those properties.

1 Like

The goal is a single library that allows for creating environments of a desired float type and solve method. It's a little frustrating I can't just say T: f64 || f32 || fwhatever.

And because I can't get something like T: AllTraitsCommonToFloats working, I've headed towards the path of using:

#[cfg(target_pointer_width = "64")] pub(crate) type FloatType = f64;

in lib.rs for the various float types and targeting to compile for each (plus optional features for different solver backends, eventually). Then I assumed I could just collect the different compiled versions and wrap them in an interface that allows selection of a base to use.

Though, honestly, no solution has felt like I'm truly going about things in the correct way.

If you want to export all the interfaces from a single crate, there’s always the jury-rigged solution:

pub mod f32 { type Float = f32; include!("implementation.rs"); }
pub mod f64 { type Float = f64; include!("implementation.rs"); }

Ah, so just forcibly including a copy of each file in a module for each given float type. Interesting!

So then... Say I have a sub-module within my crate, within its own folder and with its own root 'mod.rs' file. I'd put the following (for each variety of float) in that root mod.rs file.

pub mod f64 {
    type FloatType = f64;
    include!("main.rs");
    include!("component.rs");
    include!("another.rs");
}

Then the module would look exactly the same, except for having the prefix crate::sub_mod::f32 or crate::sub_mod::f64 before the normal contents instead of just crate::sub_mod, yes? I think that goes a long way towards what I'd like. Though I'd prefer to put the float differentiation last, such as crate::sub_mod::Component::f64. Or, if that wouldn't work, to go further in the other direction and put everything inside crate::f64 / crate::f32

But that leaves a question of how to set up generic code to correctly call the appropriately typed sub-module/components based on the set FloatType. Because, as far as I can tell, sub_mod::f64 is just a name, so sub_mod::FloatType would not work. Is it possible to set a flag to conditionally compile just the files brought in by include!() when it's run? Otherwise it seems the answer would be to use matches to access something based on the defined FloatType:

match FloatType {
    f64(_) => crate::sub_mod::f64::Item.desired_function(),
    f32(_) => crate::sub_mod::f32::Item.desired_function(),
}

as necessary throughout to keep everything connecting to the version of the correct type. Which seems like something to avoid, if possible. Better to check the type at compile than runtime...