Custom literals via const generics

I have seen a few discussions on custom literals, but I haven't seen this idea.

Literals for user-defined types would be supported for arbitrary user-defined types via traits like the following:

trait FromDecimalLiteral<const SUFFIX: &'static str> {
    fn from_decimal_literal<const V: &'static str>() -> Self;
}

trait FromBinaryLiteral<const SUFFIX: &'static str> {
    fn from_binary_literal<const V: &'static str>() -> Self;
}

trait FromOctalLiteral<const SUFFIX: &'static str> {
    fn from_octal_literal<const V: &'static str>() -> Self;
}

trait FromHexadecimalLiteral<const SUFFIX: &'static str> {
    fn from_hexadecimal_literal<const V: &'static str>() -> Self;
}

trait FromFloatLiteral<const SUFFIX: &'static str> {
    fn from_float_literal<const V: &'static str>() -> Self;
}

trait FromStringLiteral<const SUFFIX: &'static str> {
    fn from_string_literal<const V: &'static str>() -> Self;
}

trait FromByteStringLiteral<const SUFFIX: &'static str> {
    fn from_byte_string_literal<const V: &'static [u8]>() -> Self;
}

trait FromCharLiteral<const SUFFIX: &'static str> {
    fn from_char_literal<const V: char>() -> Self;
}

trait FromByteLiteral<const SUFFIX: &'static str> {
    fn from_byte_literal<const V: u8>() -> Self;
}

This would allow things like:

 // custom suffix
let a: BigInt = 239085798347502897345098273452345bigint;
// no suffix
let b: BigInt = 239085798347502897345098273452345;
// Complex numbers:
let c: Complex = 3.7 + 5.2i;
// exact decimal floating point arithmetic
let d: DecimalFloat = 1.123123123123123123123123;

Built-in integer types would implement these as well.

Thoughts?

I guess my question is, why not just the TryFrom trait in order to convert the preferred kind of literal into the desired type? What would this allow that can't be done using that route?

3 Likes

What would happen in this code?

struct Foo;
struct Bar;

impl FromDecimalLiteral<"foo"> for Foo {
  fn from_decimal_literal<const V: &'static str>() -> Self {
    Foo
  }
}


impl FromDecimalLiteral<"foo"> for Bar {
  fn from_decimal_literal<const V: &'static str>() -> Self {
    Bar
  }
}

let _ = 1foo;

You would get a compile error about ambiguous type inference. If however you had:

fn use_bar(x: Bar) { ... }

let x = 1foo;
use_bar(x);

Then it would work.

What advantage would this have over a macro?

let a = bigint!(239085798347502897345098273452345);
let b = complex!(3.7 + 5.2i);
let c = dec_float!(1.123123123123123123123123);
2 Likes

Well, one disadvantage macros have is prefix vs. suffix notation: seconds!(5) or Seconds(5) (or worse, Seconds::new(5)) vs. 5s (which C++ defines in std::literals::chrono_literals).

Custom literals may not necessarily be the solution, but I would find it nice if I could write literals for unit newtypes (like Seconds, Kilometers, Radians), in a more natural form in rust.

2 Likes

Allowing const functions in traits feels like a better fit to me:

pub trait ToSeconds {
    const fn seconds(self)->Duration;
}

impl ToSeconds for u32 { /* ... */ }

const FIVE_SEC:Duration = 5.seconds();
3 Likes

That's what I'm currently doing (except I make the user prefix an underscore in front of the digits when the numbers get really large, because the lexer complains about numbers bigger than 2^128 even in macros).

But this makes it so that user-defined types are forever second-class citizens compared to built-in types. It would be nicer to write x + 1 rather than x + bigint!(1).

Having the suffix be a string runs into the coherence issues mentioned by @Nemo157. A simple alternative is to just look up the suffix as a type. Combining that with const functions as mentioned by @2e71828, using an inline const expression

123u32 would desugar to const { u32::from_decimal_literal("123") }

0x123Foo would desugar to const { Foo::from_hexadecimal_literal("123") }

Admittedly, suffixes starting with a capital letter look ugly, so this would encourage the use of type names starting with a lowercase letter (or at least type aliases, e.g. type foo = Foo if your type was Foo but you wanted to be able to use the suffix foo). This may or may not be a problem.

3 Likes

You can already do something quite similar with extension traits. I've done it for the time crate, at least. Honestly the only thing that would be helpful here is having impl const Foo, which is already on nightly. This would allow trivial situations like mine to be done at compile-time. Combined with const blocks and/or more const evaluation for optimization, this would effectively result in custom literals.

https://time-rs.github.io/time/ext/trait.NumericalDuration.html

1 Like

Unfortunately... hex literals with suffixes are hard. The logical parse for that would actually be const { oo::from_hexadecimal_literal("0x123F") }. abcdef are valid hex digits so part of the number, not the suffix. (I believe this is part of why hex floats aren't allowed: suffixing them with f32 wouldn't work as expected, because those are all hex digits.)

1 Like

It's not a bug, it's a feature! We want the type to be able to be deduced based on context, just like today built-in integer types are deduced based on context.

In particular this is important for the case of the empty suffix, where it would be nice if it could not only be a built-in integer, but some user defined type as well like a BigInt or a Gaussian integer.

If we ever get type ascription, then that gives a more distinct syntax: 123:u32 or 123:BigInteger or whatever.

3 Likes

That's already a thing being actively developed FYI, which you can use on nightly currently with the const_trait_impl feature flag. It works differently than in your example though (in a good way, I'd say). That is, what you do is just:

impl const LiterallyAnyTrait for SomeType.

Meaning trait authors don't have to do anything differently (since marking trait methods explicitly as const fn is not part of how the feature works at all), and the feature is immediately compatible with all traits ever, with the only limitation being that of people's abilities / desire to actually come up with useful const-compatible implementations of them.

Here's a playground example that IMO demonstrates why the feature would be a lot worse if it depended on a given trait to be "written as const", since nothing like ConstAdd or ConstAddAssign or what have you exists obviously.