Hacker News new | ask | show | jobs
by pharmakom 1757 days ago
Is this a scrappy version of type classes (Haskell) or traits (Rust)?
1 comments

Sort of, in that they are a thing you do to constrain generic parameters.

A significant difference in my understanding is that type classes and traits are required, but concepts are not. That is, using the example from the article, a concept can tell you if you're passing something that doesn't add, but you can add inside a function without using a concept. In other words:

    fn add<T>(x: T, y: T) -> T {
        x + y
    }
This won't compile in Rust:

    error[E0369]: cannot add `T` to `T`
     --> src/lib.rs:2:7
      |
    2 |     x + y
      |     - ^ - T
      |     |
      |     T
      |
    help: consider restricting type parameter `T`
      |
    1 | fn add<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
      |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
This suggestion works, but you don't have to write it this way. Once the constraints get more complex than T: Foo, I personally switch to this form:

    use std::ops::Add;
    
    fn add<T>(x: T, y: T) -> T
    where
        T: Add<Output = T>,
    {
        x + y
    }
    
I find it a little easier to read. YMMV.

Whereas in C++, this does compile:

    template<typename T>
    T add(T a, T b)
    {
      return a + b;
    }
If you try to add something that doesn't have + defined:

    int main(void) {
        add("4", "5");
    }
you get this

  <source>:4:12: error: invalid operands to binary expression ('const char *' and 'const char *')
    return a + b;
           ~ ^ ~
  <source>:8:5: note: in instantiation of function template specialization 'add<const char *>' requested here
      add("4", "5");
      ^
Whereas, if you do what the article does (though I'm using char* instead of std::string, whatever)

    #include <concepts>
    template<typename _Tp>
    concept integral = std::is_integral_v<_Tp>;
    
    template<std::integral T>
    T add(T a, T b)
    {
      return a + b;
    }
    
    int main(void) {
        add("4", "5");
    }
you get

    <source>:12:5: error: no matching function for call to 'add'
        add("4", "5");
        ^~~
    <source>:6:3: note: candidate template ignored: constraints not satisfied [with T = const char *]
    T add(T a, T b)
      ^
    <source>:5:15: note: because 'const char *' does not satisfy 'integral'
    template<std::integral T>
                  ^
    /opt/compiler-explorer/gcc-11.1.0/lib/gcc/x86_64-linux-gnu/11.1.0/../../../../include/c++/11.1.0/concepts:102:24: note: because 'is_integral_v<const char *>' evaluated to false
        concept integral = is_integral_v<_Tp>;
                           ^
This doesn't feel like a huge change because add is such a small function, but if it were larger and more complicated, the error with a concept is significantly better.