The often useful typeof

Taking a look at this commit:

https://github.com/rust-lang/rust/pull/50981/files

I see:

-        self.get() != usize::MAX
+        self.0 != u32::MAX

This kind of situation is not bug-prone because if you change the type of self.0 the equality becomes a type error.

But I'd like to write code like that in a more DRY way, something like this:

self.0 != typeof(self.0).max_value()

Or even:

self.0 != typeof(self.0)::MAX

I guess you can approximate this (somewhat verbosely, but perhaps not too bad with macros) as:

trait Max {
    const MAX: Self;
}

impl Max for usize {
    const MAX: Self = usize::MAX;
}

struct Bar<M>(M);

impl<M: Max + PartialEq> Bar<M> {
    fn foo(&self) -> bool {
        self.0 != M::MAX
    }
}

Also, I think that self.0 != u32::MAX is not all that bad because Rust doesn't allow implicit bitwidth changes. So if you change the field to something other than u32 it won't compile and you'll need to update that place. So the typeof wouldn't buy you much in this particular case.

I'd more like to see decltype style of referring to synthetic types.

That's a bit silly, but:

self.0.checked_add(1).is_some()
2 Likes

Personally, I'm happy to see that it compiles down to the same thing as self.0 != u32::MAX :slight_smile:

1 Like

Note that MAX is actually a constant in the module std::u32, not an associated constant of the primitive type. So a theoretical typeof(expr) could only enable the former max_value().

2 Likes

Right, it's not bug-prone, but it still requires you to perform a change that is avoidable with more DRY code.

you could consider something like:

trait Max: Copy + PartialEq {
    const MAX: Self;
    
    // _should_ be optimized to be just Self::Max
    #[inline(always)]
    fn max(_: Self) -> Self { Self::MAX }
    
    #[inline(always)]
    fn is_max(self) -> bool { self == Self::MAX }
    
}

and then use Max::max(self.0) for using max or
in your case self.0.is_max().

Through I agree having a typeof like construct would not be
so bad, through it would not help you as MAX is implemented
on the module u32, not the primitive type. (So it would be something
like <typeof(self.0) as NumMeta>::MAX).

PS:
impl for Max

macro_rules! impl_max {
    ($($tp:ident),*) => ($(
        impl Max for $tp { const MAX: $tp = $tp::max_value(); }
    )*);
}

impl_max!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);

`

1 Like

DRY is not really a general programming principle per se. It's only worth advocating for when it serves a real purpose: avoiding actual errors in the code. Otherwise it is just another layer of textual indirection that people have to think about, and it avoids the many useful and pedagogical reasons to write code that repeats itself.

With that said, there is probably a reason typeof doesn't currently exist in rust (or most modern languages) and I'd like to attempt to highlight the reasoning for that. To many of us, it feels archaic and needlessly imperative.

Anyway it all begins with what you think a prgram is. If you think of a program as a syntactic construct that instructs the compiler about what to do, then naturally typeof makes sense. You're saying "Hey compiler. I gave you an expression, and I know you've got type info on it in your memory banks, so do that lookup and substitute the result for this expression."

It makes perfect sense if you're the kind of person who looks at a program as a tree of text and "substitution of one part for another" as a primitive operation.

But if you think of a program as a semantic contruct which upholds invariants and encodes notions of behavior, then it doesn't make sense. What would instead make sense is to declare your intent to make one expression depend on the type of another, and let the compiler expose the relationship through a language construct. (Rust already does this kind of thing using traits.)

But these differences are not wholly symmetrical, because in the first case, "telling the compiler what to do" makes it really hard to write a compiler. Now you have to think of edge cases like "what if they use typeof of a captured closure variable on a trait constraint in a generic parameter of a triply nested function definition?" And the answer will either be "let's forbid that", so now typeof doesn't work everywhere users expect it to, or "well, we better expose every decision the compiler makes to the user so they can construct any software that fits their fancy, despite the fact that we're going to basically add a whole giant runtime and reflection system to the language to do so."

I'm fairly certain a modern version of typeof should look like a declaration-in-a-context so as to not give anybody the impression that it is a fundamental type system primitive that lets people write their own ad-hoc typesystem atop Rust's expression machinery.

5 Likes

DRY is not really a general programming principle per se. It’s only worth advocating for when it serves a real purpose: avoiding actual errors in the code.

Some redundancy in the code is useful as "double booking" and to increase readability. Natural languages are full of redundancy and that's good. On the other hand there are many situations where I prefer to not state things two times, because often they go out of sync. In the case shown in my original post it's not going to cause a bug, but it causes a code change that for me is not needed. In Ada language and D language in that case you don't need to restate the type of the variable with "u32::MAX". In the end it's just a choice, and my choice is that I usually prefer my code DRY.

With that said, there is probably a reason typeof doesn’t currently exist in rust (or most modern languages)

Often things are not in Rust because no one implemented them. Currently typeof is a reserved keyword but it's not implemented yet. And coming from D language, I miss its functionality. It's not necessary because you can often introduce a type alias.

Traits are awesome, but I think that's a bit heavy for the one shot case I've shown above. Perhaps you have something else in mind?

The successive things you write are a bit too much complex for me (they fly above my currently tired head), thank you for your note.

2 Likes

I understand that; what I'm trying to get at is that no one implemented this because it is hard, and it is hard because you're taking an almost entirely declarative type system and introducing an imperative way to interact with it.

typeof certainly is useful -- I'm not debating that -- and there's probably a real need for it in some cases, but there are likely better ways to solve the problems it is meant to solve.

1 Like

I've read your post again, and I think I understand now. If typeof() introduces so much complexity in the compiler then better to leave it out.

Well, it might. I personally don't know that for a fact, but I suspect if it didn't, we would already have it. What I mean is... it doesn't seem that hard to add to the compiler, it just might introduce a large number of constraints on what the compiler is allowed to do and assume, which could very easily make it harder to to do other things we want the language to do. So it's something I'd be nervous about.

A type alias might be sufficient for basic cases like this as well.

1 Like

I wonder if typeof would really be a good idea in a type system like Rust's where the type of an expression depends on the context in which it is used.

This has become a more pressing issue now that impl Trait lets you write functions with unnameable return types. Some people think typeof is ugly, but personally I think its benefit-to-weight ratio is quite high. And while it does add implementation complexity to support expressions written in the middle of types, that's something rustc will need to be able to do anyway to support const generics – or even to properly implement the existing language, since you can write expressions as the lengths of fixed-size arrays, something which is currently buggy when combined with generics…

2 Likes

A bit late to the party, but it seems to me that traits are great at this use case specifically.

For instance, use the Bounded trait from the num crate:

use num::Bounded;
self.0 != Bounded::max_value()
2 Likes

I'm not sure I understand the relevance of impl Trait here... I'm imagining something like typeof(t: impl Trait), but it seems that you just wouldn't use impl Trait here, or that you wouldn't be able to do anything with the returned type anyway... If impl Trait acts like an anonymous generic type parameter, what methods and traits would you expose after you use typeof on it?

Am I misunderstanding something?

@comex is probably alluding to situations discussed in https://github.com/rust-lang/rfcs/issues/1738

I think it just would be nice if MIN and MAX were placed in some kind of Bounded trait, as Haskell does it with class Bounded, then we could add bounds for almost all std types, even bool.

1 Like