user133831
user133831

Reputation: 670

How do I check a trait constraint at runtime to display a value?

I am trying to find the "correct" Rust approach to this problem.

The original problem I faced was that I have a method that gets called to print out some values, the exact format depending on a number of options. I needed to add support for some new options. Not all options work with all types of values but there are runtime checks to confirm the options in use are valid for the values passed before my method is called.

If I was doing it in C I would create a printf style format string at runtime, based on the options, and call printf. print! et al don't allow runtime defined format strings though. My next idea was to have a series of print! calls with different static format strings and use if or match to control which was called.

My simplified version of this is:

fn print<T>(number: T, hex: bool) -> ()
where
    T: std::fmt::Display + std::fmt::LowerHex
{
    if hex {
        println!("{:x}", number);
    } else {
        println!("{}", number);
    }
}

fn main() -> ()
{

    print(10, false);
    print(10, true);
    //print(3.1415, false);
}

If I uncomment the float however then this fails to compile because floats don't implement the LowerHex trait. We know that it is safe though, as hex is false and so the trait won't be needed - but I'm not sure if the compiler can be convinced of this?

I could presumably create a trait that has a method for each of the different sorts of format strings needed and then implement it for (in this case) both ints and floats, but in the float case call panic! in methods that need hex conversion. That seems a bit hacky (and very verbose beyond this simple example) though.

I could presumably also use unsafe to call printf from libc. Or implement my own formatting routines. Neither seem like they are the proper approach.

Is there a nicer solution? I guess the fundamental problem is that floats and ints are both numbers to a human but totally different beasts when it comes to formatting.

Upvotes: 0

Views: 834

Answers (3)

eggyal
eggyal

Reputation: 125855

You can add a layer of indirection with (fallible) coercion to trait objects:

use std::fmt;

trait AsLowerHex {
    fn as_lower_hex(&self) -> Option<&dyn fmt::LowerHex>;
}

impl AsLowerHex for f32 {
    fn as_lower_hex(&self) -> Option<&dyn fmt::LowerHex> {
        None
    }
}

impl AsLowerHex for i32 {
    fn as_lower_hex(&self) -> Option<&dyn fmt::LowerHex> {
        Some(self)
    }
}

fn print<T>(number: T, hex: bool) -> ()
where
    T: fmt::Display + AsLowerHex
{
    if hex {
        println!("{:x}", number.as_lower_hex().unwrap());
    } else {
        println!("{}", number);
    }
}

fn main() -> ()
{
    print(10, false);
    print(10, true);
    print(3.1415, false);
}

Playground.

Unfortunately, that does require one to explicitly implement the conversion for every type, which can be resolved with the (currently incomplete/unstable) specialization feature:

#![feature(specialization)]

impl<T> AsLowerHex for T {
    default fn as_lower_hex(&self) -> Option<&dyn fmt::LowerHex> {
        None
    }
}

impl<T: fmt::LowerHex> AsLowerHex for T {
    fn as_lower_hex(&self) -> Option<&dyn fmt::LowerHex> {
        Some(self)
    }
}

Playground.

Upvotes: 1

user31601
user31601

Reputation: 2610

I don't know if this is what you're looking for (it depends a bit on how many "options" you have, and what combinations are valid), but one choice would be "encode" the options in the value themselves.

Something like this:

use std::fmt;

fn print<T>(number: T)
where
    T: fmt::Display
{
    println!("{}", number);
}

// The 'Hex' struct encodes the 'option' to print something as
// hexadecimal with the value itself.
struct Hex<T>(T);

impl<T> fmt::Display for Hex<T>
where
    T: fmt::LowerHex
{
    
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:x}", self.0)
    }
    
}

// You can then use it like this

fn main() -> ()
{
    print(10);
    print(Hex(10));
    print(3.1415);
}

// Output:
// 10
// a
// 3.1415

Playground

This makes use of the Newtype idiom often used in Rust, which is a zero-cost abstraction. Also, all the "checking" is done at compile time, so the runtime code is very efficient.

The obvious downside is that the 'options' must be specified at compile time (i.e. one cannot infer whether hex should be true or false by reading a value at runtime).

Upvotes: 1

E_net4
E_net4

Reputation: 29983

There appear to be two things in the compiler which are stopping you from achieving this:

  • It is not possible to apply type conditional type constraints. If there was such thing as a conditional type in Rust, you would probably be able to describe a constraint of the form T: std::fmt::LowerHex if hex. But such a feature is not something that can be easily outlined for implementation into the language, nor do I know of an RFC proposing something of this sort.
  • It is also not possible to check whether a value implements a trait at run-time. There are some run-time reflection capabilities in the Any trait, but not one which could let you identify whether a value also implements a trait. This is because traits are concepts which only exist at compile time in their full essence.

To solve this case, you have already outlined a fairly reasonable course of action:

I could presumably create a trait that has a method for each of the different sorts of format strings needed and then implement it for (in this case) both ints and floats, but in the float case call panic! in methods that need hex conversion. That seems a bit hacky (and very verbose beyond this simple example) though.

A new trait which handles hex for float types in a different way would work. If you are OK with using unstable features, it might be possible to narrow down the number of impl statements needed through specialization. A forewarning is that there is no stabilization of this feature in sight, even after several years since its presence in the nightly compiler.

Alternatively, if you can handle using two functions instead of one at the call sites, then this would be a possible workaround.

fn print_hex<T>(number: T) where T: Display + LowerHex;
fn print<T>(number: T) where T: Display;

See also:

Upvotes: 2

Related Questions