[HN Gopher] Const Generics in Rust
       ___________________________________________________________________
        
       Const Generics in Rust
        
       Author : todsacerdoti
       Score  : 83 points
       Date   : 2021-11-06 16:33 UTC (6 hours ago)
        
 (HTM) web link (nora.codes)
 (TXT) w3m dump (nora.codes)
        
       | quelltext wrote:
       | Will this make Rust's type system dependently typed like Agda or
       | Idris?
        
         | lmkg wrote:
         | Const Generics are a form of dependent typing, but a heavily
         | restricted one. This will loosen some restrictions, increasing
         | what can be done with dependent types, but still a far cry from
         | languages with "full" dependent types.
        
         | ReleaseCandidat wrote:
         | No. To have dependent types the values the types depend on must
         | be runtime constants, not compile time constants.
        
           | zozbot234 wrote:
           | Strictly speaking, dependently-typed languages don't even
           | have a clear-cut phase separation between "compile time" and
           | "run time" (other than via "code extraction" to a non-
           | dependently typed language as an add-on feature). It's clear
           | however that the Rust const-generics 'MVP' is quite far from
           | the flexibility of full dependent types.
        
         | 3r8Oltr0ziouVDM wrote:
         | No, const generics only work with compile-time constants.
        
       | Waterluvian wrote:
       | As a bit of a type amateur, I wondered about this: why do we
       | define primitive types and leave it at that? Why aren't things
       | like min and max first class constraints one can place on a type?
       | 
       | I imagine this basically gives you that. But do other languages
       | do this? I'm guessing maybe functional programming languages?
        
         | smegcicle wrote:
         | Like what ReleaseCandidat said--- types originated as a
         | compile-time thing, and things like range checks mostly make
         | sense as run-time assertions, so it seems kind of strange from
         | a compiler standpoint to conflate them...
         | 
         | But, from the developer's standpoint, types are a very
         | convenient place to put such run-time assertions. Raku has
         | them.
        
         | Vecr wrote:
         | Ada does. It automatically selects the backing type depending
         | on what range you specify.
        
           | foobiekr wrote:
           | Since I encountered Ada in the late 80s, I've often wondered
           | how this would work out over a long lived project, even when
           | I was young and full of confidence and the expectation that
           | long lived was going to be a thing of the path.
           | 
           | C has been a headache in many ways, but one thing I've
           | noticed is that there are many examples of very, very long-
           | lived C projects, and not very many examples of very, very
           | long-lived projects in most other languages (IBM, obviously,
           | will disagree here, and COBOL). Some of that is related to C
           | being a good fit for generating economic value at the
           | beginning of the era that would now qualify as very long
           | lived (20+ years, 30+ years), I'm sure, but it is interesting
           | to consider that parts of some languages will not age well.
        
             | ReleaseCandidat wrote:
             | > there are many examples of very, very long-lived C
             | projects
             | 
             | Of course there are, because that's the 'native' language
             | for all Unix' and Unix-lookalikes and Windows because of
             | the Kernels in C. So, for any OS relevant for 'real'
             | computers of the last 30 years. Had the Lisp-Machines won
             | the battle, nobody would talk about C any more (or just to
             | scare children like with COBOL). And don't forget about
             | Fortran, there is some _really_ ancient Fortran code in use
             | at the same places that still use their COBOL.
        
         | tinco wrote:
         | Because it's hard and ultimately imperfect. Even a simple
         | constraint as a max value (which inherently all integer values
         | should have) requires the compiler to have good understanding
         | of the code for it to recognize explicit bounds checks on the
         | value. If there's no explicit bounds checks it would have to
         | prove it through inference which is often (usually?) impossible
         | even if the compiler was smart enough to do so.
        
           | ReleaseCandidat wrote:
           | > [...] max value (which inherently all integer values should
           | have)
           | 
           | No, they shouldn't. Would really prevent many subtle bugs if
           | the default integers of every language would be arbitrary
           | precision ints.
        
             | willvarfar wrote:
             | Python is an example language with "big integers".
             | 
             | Things quickly get interesting interoperating with other
             | languages eg via json.
        
         | ReleaseCandidat wrote:
         | Dependent types is what you're searching for. Mostly used by
         | (functional) proof 'languages' like Coq, Agda, Idris. For a
         | language to have dependent types, the values that the types
         | depend on must not (like with Rust) only be compile time
         | constants, but values at runtime.
        
           | tialaramex wrote:
           | Huh, surely the types in WUFFS are offering what the parent
           | wanted (WUFFS won't let you add two 32-bit signed integers x
           | and y together unless you've explicitly constrained them so
           | that this operation won't overflow or underflow) but purely
           | enforced at compile time since WUFFS wants to go real fast?
           | 
           | WUFFS is in a related place to something like Coq. The idea
           | in WUFFS is, if I write my decoder for the FOO image format
           | in WUFFS, no matter how lazy and incompetent I am, your image
           | displaying program can confidently use my FOO decoder, and if
           | I did a bad enough job it might be _slow_ or produce the
           | wrong _picture_ but it can 't crash or run a SSH server or
           | delete the password database or whatever.
        
         | willvarfar wrote:
         | This was common in Wirth languages eg Pascal, where they are
         | called "sub-range types"
         | https://wiki.lazarus.freepascal.org/subrange_types
         | 
         | It's something I actually miss in modern mainstream languages.
         | But as it's something few copied and few lament, we can
         | extrapolate that I'm a minority :)
         | 
         | C99 has something similar but different in that integer fields
         | in a struct can have a bit width specifier. This is is
         | primarily used for bit fields in protocol frames and things,
         | and conventional wisdom is that compilers generate poor code
         | for them even today, and doing your own bit masks instead is
         | still popular.
        
           | Animats wrote:
           | I wanted that in Ada, which has subranges. I wanted "ranges"
           | instead - no mention of the underlying hardware at all.
           | Intermediate values would be sized based on the rule that you
           | can't overflow an intermediate value unless it would lead to
           | overflow of the final sized value. Sometimes this requires
           | longer intermediate values.
           | 
           | What happened instead was standardized hardware. The 60 bit
           | CPUs, the 48 bit CPUs, the 36 bit CPUs, the 24 bit CPUs, the
           | 18 bit CPUs, and the 12 bit CPUs all died off. With everybody
           | using powers of 2 of bytes, range calculation became less
           | important for portability.
        
           | auxym wrote:
           | Nim's type system was inspired by Wirthian languagrs and it
           | does have subranges.
        
       | gsliepen wrote:
       | C++ has const generics, and they seem to be a lot more powerful,
       | as you can use expressions in template parameters, as long as
       | they can be evaluated at compile time of course:
       | 
       | https://godbolt.org/z/7Tbzhx5Kc
       | 
       | Rust is a very nice language, but I feel they forgot to copy some
       | useful features from other languages.
        
         | NieDzejkob wrote:
         | They didn't "forget" to do it, it's just not finished yet.
        
           | cyber_kinetist wrote:
           | The reason: Rust has a different view of how to design type
           | systems compared to C++. They want their type system to be
           | logically more grounded (as the tradition in ML languages
           | like Haskell) rather than the "screw it" template approach.
           | So they're trying to make const generics logically sound
           | (basically a very limited version of dependent types) and
           | integrate well with the rest of the type system, which is why
           | it's taking so long.
        
         | cyber_kinetist wrote:
         | Rust's generics feels a lot more rigid than C++ templates. In
         | C++ template programming works more like a duck-typed dynamic
         | language (with very esoteric "runtime-like" compile time error
         | messages), while Rust's type system feels a bit more rigid
         | probably due to its influences from ML-like functional
         | languages. Though there are tradeoffs with each approach: C++
         | has gained an incredible notoriety for all sorts of wacky
         | template tricks (SFINAE, template metaprogramming, etc...)
         | Thankfully they developed features like constexpr and concepts
         | to remedy some issues with it (the one part of recent committee
         | decisions that I like, though I generally don't like where the
         | language is going overall)
        
           | tialaramex wrote:
           | Yeah. Take for example the thinking on const generics for
           | user-defined types.
           | 
           | Right now in the Const Generics MVP you can only use integer
           | types. So you can write a generic argument with an u32 N in
           | it, or a char C (a Unicode scalar, so basically a "character"
           | in Unicode) or something like that. There are a bunch of
           | practical uses for this, but obviously there's no reason in
           | principle it must be so limited.
           | 
           | In C++ there's always a temptation to just rip the knob off,
           | sure it might be a _terrible idea_ to make a new type generic
           | over the 32-bit floating point numbers, but that 's _your_
           | problem as the developer right? Just don 't do that if you're
           | uncomfortable cleaning up the mess. Rust does not like to
           | provide so many foot guns.
           | 
           | So the last time I looked the proposal was that user-defined
           | types could be eligible for const generic treatment if they
           | _derive_ Eq.
           | 
           | At the first glance that sounds correct, if the type is Eq
           | then it can make sense to be generic over it since apparently
           | we can tell when A = B and thus whether Foo<A> is actually
           | the same as Foo<B> or different.
           | 
           | But then an extra thought brings you up cold. I can claim my
           | type is Eq with one line if it's PartialEq. And I can _claim_
           | my types are PartialEq just by providing a bogus eq()
           | function that always says false. So isn 't this a useless
           | requirement?
           | 
           | And then you study the language more carefully. They're going
           | to require that you get Eq by deriving it. Anybody can
           | implement PartialEq as I described above, but that's not
           | _derived_. To get a _derived_ PartialEq (and thus Eq) the
           | derive macro has to allow that, and that 's only going to
           | work for the kind of composite types a non-crazy person would
           | use for const generics.
           | 
           | Your trivial enumerated type with a list of fifteen types of
           | chocolate bar can easily _derive_ Eq if you want to write
           | const generics so that somebody can make a variable of type
           | AdventCalendar <Chocolate::Mars> in this future Rust version,
           | but my custom type that contains two pointers to C Strings
           | can't successfully derive Eq and so isn't eligible to try to
           | be used for const generics even if it really genuinely
           | _implements_ Eq.
        
             | gsliepen wrote:
             | Actually, C++ does limit the non-type template
             | parameters[0] to integral types, enums and function
             | pointers. But some people have found that too restrictive
             | and would like to use other constants as well, like string
             | literals.
             | 
             | [0]: https://en.cppreference.com/w/cpp/language/template_pa
             | ramete...
        
               | tialaramex wrote:
               | That document, which I've read, lists "a floating-point
               | type" as allowed since C++ 20.
        
           | zozbot234 wrote:
           | Rust uses proc macros for compile-time programming, not
           | generics. These are quite distinct features, with distinct
           | purposes.
        
         | preseinger wrote:
         | A language is not just the sum of its features, it's a
         | composite whole. Features interact, and if a language is to be
         | good, those interactions need to be designed rather than
         | incidental. Adding a new feature can very easily deliver net
         | negative value!
        
       | ww520 wrote:
       | Can this be used somehow for the bounded range? some like,
       | struct MyIndex<const MIN: usize, const MAX: usize> {
       | index: usize,  MIN/MAX??         }
       | 
       | That would get rid of lots of runtime bound checks.
        
         | estebank wrote:
         | Not as things stand now. For this functionality you would want
         | something closer to `I32<0..500>` and `struct I32<const RANGE:
         | Range>(i32)`, so then operations between two `I32<RANGE>` can
         | check what each corresponding const param is. I don't know if
         | this will be possible in the short term, but I know that quite
         | a few people want something along these lines, so I think it
         | might happen at some point.
        
           | ww520 wrote:
           | Yes, that would be ideal. Was just thinking whether the const
           | generics can be hacked to do the same thing.
        
             | estebank wrote:
             | You can't use field access today, so having a const param
             | `struct Range<T> { start: T, end: T }` isn't yet useful,
             | but you can still get something working, although the error
             | message is _terrible_ :
             | 
             | https://play.rust-
             | lang.org/?version=nightly&mode=debug&editi...
             | 
             | Edit: I'm honestly surprised at how usable this is already
             | in the current state of nightly:
             | 
             | https://play.rust-
             | lang.org/?version=nightly&mode=debug&editi...
        
       | ChrisSD wrote:
       | I would be cautions of claiming performance improvements without
       | the benchmarks to back it up. The compiler is often very good at
       | figuring out if a length is constant and optimizing
       | appropriately. I suspect it'd be able to do it in the simple
       | examples given. But maybe I'm wrong!
       | 
       | Admittedly it can be nice not to be at the mercy of the
       | optimizer. And getting compiler errors instead of runtime panics
       | is a really great benefit. But I've just become slightly wary of
       | big performance claims these days. Perhaps I'm just getting too
       | cynical.
        
       | Animats wrote:
       | It's a little too early to be hyped about const generics in Rust.
       | You still can't do basic compile time arithmetic on them, such as
       | (BITS+7)/8 to get how many bytes you need.
       | 
       | I recently had to use "FixedBitSet" in Rust. This is simply a set
       | of N bits. It's the clean way in Rust to do bit-banging.
       | Unfortunately, the underlying implementation is a dynamically
       | allocated Vec. So, when you do bitset.set(4,true), far more code
       | is generated than just one OR instruction.
       | 
       | So, it's not quite here yet.
        
         | SV_BubbleTime wrote:
         | > far more code is generated than just one OR instruction.
         | 
         | How many?
         | 
         | Because looking at ARM and C with an unpacked bitfield
         | structure, you'll have 3 or 4 easily. A load, some shift, or,
         | store. Anytime I see a bitfield in C I assume 4 instructions.
         | So is this close?
        
         | Shish2k wrote:
         | I've been using the `bitflags` crate to do my bit-banging, and
         | it seems to compile down to a single OR instruction quite
         | nicely :)
        
         | pcwalton wrote:
         | I wonder if it'd make sense to have the const generic parameter
         | be the number of bytes (or words) that the bit set needs,
         | instead of the number of bits. It's ugly, but it could allow
         | the optimizer to eliminate many runtime bounds checks as the
         | size of the vector would become a compile-time constant.
        
       ___________________________________________________________________
       (page generated 2021-11-06 23:02 UTC)