Does rust have something like `Symbol` in JavaScript?

Introduction: Symbol

TLDR: Symbol is used to create unique value.

I want to create some constants and each of them has a unique value like below:

#[derive(PartialEq, Eq)]
struct Symbol;
const A: Symbol = Symbol;
const B: Symbol = Symbol;
// C is also a symbol
use external_crate::C;

Here for the equivalence relation among A, B, C, you can consider them as 1, 2, 3.

Alternatives I've considered

Uuid

No, because it doesn't support const fn.

enum

No, because the variants number of an enum is fixed. However for my needs, the code may import a symbol from an external crate.

Might I ask what for? I don't see an immediate use-case for Symbol in a statically typed language. According to the MDN docs, Symbol is used

to add unique property keys to an object that won't collide with keys any other code might add to the object, and which are hidden from any mechanisms other code will typically use to access the object.

Rust does not have a mechanism for "other code" to add keys to an object. And if you want to restrict "access to the object", you can keep the fields you don't want accessible private.

That being said, if you want two instances of Symbol to never equal each other, you could implement PartialEq in a way that reflects that:

#[derive(Debug)]
struct Symbol;

impl PartialEq for Symbol {
    fn eq(&self, _: &Self) -> bool { false }
}

const A: Symbol = Symbol;
const B: Symbol = Symbol;

fn main() {
    assert_ne!(A, B);
}

Playground.

3 Likes

In order to create guaranteed distinct items at compile time, you have to declare a new item of some kind; there is no way to have merely a const fn or other constant expression that returns a new distinct value when called. This means that creation of a symbol will need to be done through a macro. The two ways that come to mind to obtain distinct values are the TypeId of a new type or the address of a new static. Here is how to do it with a static:

use core::fmt;

#[derive(Clone, Copy)]
pub struct Symbol(pub &'static SymbolInner);

// The field is not used for anything but must be at least one byte.
// <https://doc.rust-lang.org/reference/items/static-items.html#r-items.static.storage-disjointness>
#[doc(hidden)]
pub struct SymbolInner(#[allow(unused)] u8);

macro_rules! symbol {
    () => {
        {
            static UNIQUE: SymbolInner = SymbolInner(0);
            Symbol(&UNIQUE)
        }
    }
}

impl PartialEq for Symbol {
    fn eq(&self, other: &Self) -> bool {
        // Compare the addresses of the `SymbolInner`s.
        core::ptr::eq::<SymbolInner>(self.0, other.0)
    }
}
impl Eq for Symbol {}

impl fmt::Debug for Symbol {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Symbol({})", core::ptr::from_ref(self.0).addr())
    }
}

#[test]
fn equality() {
    const A: Symbol = symbol!();
    const B: Symbol = symbol!();
    let a = A;
    assert_eq!(a, A);
    assert_ne!(A, B);
}

This way also allows you to create symbols at run time but you have to Box::leak() to create a unique address for each one. A TypeId version would not allow creating symbols at run time at all, but would avoid needing a static allocation of one byte per symbol.

9 Likes

I don't see an immediate use-case for Symbol in a statically typed language.

I am using Rust to writing a draft of a conlang, where a word corresponds a constant, so every constant should have a different value. It's surely ok to use string directly to compare them (const HELLO: &str = "hello"), but since the constant name has already implied its meaning, writing it again would be just repeated work. So I was looking for a way to simplify it like Symbol.

1 Like

If you want to generate unique values during runtime, you can do it without leaking by just using a static counter:

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
struct Unique(u64);

impl Unique {
    fn new() -> Unique {
        static COUNTER: AtomicU64 = AtomicU64::new(0);

        Unique(
            COUNTER.fetch_add(1, Ordering::Relaxed)
        )
    }
}

This way you will not leak memory. This solution does not protect against the counter wrapping, but it should not be an issue (2^64 is a lot of IDs).

12 Likes

Thanks to @kpreid 's inspiration, I just tried solving this problem again using the TypeId method. Here's the code:

use quote::quote;

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Symbol)]
pub fn symbol_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let ident = input.ident.clone();
    
    quote! {
        impl symbol::Symbol for #ident {}

        impl<T: symbol::Symbol + 'static> PartialEq<T> for #ident {
            fn eq(&self, other: &T) -> bool {
                std::any::Any::type_id(self) == std::any::Any::type_id(other)
            }
        }

        impl Eq for #ident {}

        impl std::fmt::Debug for #ident {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.debug_struct(stringify!(#ident)).finish()
            }
        }
    }.into()
}
pub use symbol_derive::Symbol;

pub trait Symbol {}
use symbol::Symbol;

#[derive(Symbol)]
struct A;

#[derive(Symbol)]
struct B;

fn main() {
    assert_eq!(A, A);
    assert_eq!(B, B);
    assert_ne!(A, B);
    assert_ne!(B, A);
}
1 Like

That achieves the desired PartialEq behavior, but makes the symbols hard to actually do anything with since each one has a distinct type and they can't ever be passed around as a single type. I had in mind something more like the static version:

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Symbol(pub TypeId);

macro_rules! symbol {
    () => {
        {
            struct Unique;
            Symbol(TypeId::of::<Unique>())
        }
    }
}

Now that I’ve written it down, it seems to me better in all ways than the static version. I had a vague notion that it would cost more compile time, but that’s probably unjustified.

1 Like

This method doesn’t support creating multiple unique symbols in a loop (unsurprisingly, as that’s a runtime thing). I’d probably add something like a static std::sync::Once-based guard into the macro expansion so that this kind of misuse panics instead of producing an unintentional duplicate.

1 Like

It's bigger (TypeId is 128 bits) and may also get bigger and/or harder to compare in the future.