Traits as types of types

If you’re thinking of it that way, then the equivalent of the number line is the complex plane. Just like the integers lie on the real line, the real and imaginary lines lie on the complex plane, and just as the integers barely cover the real line (in fact 0% of the real line consists of integers) the real and imaginary lines barely cover the complex plane.

Whether real numbers are complex, and also whether integers are real, depends on how you define them, so in truth, we’re both correct depending on which definition you use.

1 Like

The average person would not think twice before referring to "2 apples and no bananas" as "2 apples". You can't add apples and bananas. Well, you can if you classify them both as fruits, 2 apples + 2 bananas = 4 fruits.

Apples, bananas, fruits, all different things. As are real, imaginary and complex numbers.

I'm not worried abut representation here. We seem to have three different things no matter how you write it.

Yes indeed.

Which is a very different thing. One can compare real numbers and put them in order. That is what the number line is all about. There is no such ordering for complex numbers. Ergo I claim they are very different types.

I'm using my definition :slight_smile:

Whether the real number 1 and the complex number 1+0i are "the same thing" is mostly just a semantics game.

In mathematics, we generally treat them as identical, where the objects we call "real numbers" are just a subset of complex numbers. This is because all of the properties of 1 and theorems that apply to 1 also apply to 1+0i. Treating them as separate things would only add an annoying source of uninteresting complexity to all mathematical discussion of either thing.

In programming languages with type systems, we generally treat them as different, often simply because they have different implementations (with observably different properties like memory footprint). And even if their implementations were the same, it's often less error-prone to be specific about types in code that humans are meant to read, write and run on their computers. But math never has to think about "implementing" a type (unless you count "implementing" stuff in a mathematically idealized context like a Turing machine) or about "clients running" it.


Going back to the original question...

In the simplest and broadest possible sense, a mathematical "type" is just a set of values. No other restrictions.
{ 1, 2, 3, ... } is a type
{ 1 } is a type
{ 1, 3, 5, ... } is a type
{ apple, orange, kumquat } is a type
{ 1, kumquat, 42 } is a type

Programming languages usually define a "type" more restrictively than this for a bunch of practical reasons.

But in the original quote you were asking about:

Generic types describe ranges of types, just like types describe ranges of values, and traits are to generic types what properties are to structs. So a new trait actually defines a new type of types, and can be combined with other traits to form a hierarchy of traits.

I assume "type", "range" and "property" are all meant to be read in an extremely broad sense, effectively as synonyms for "set", as this is often how they're used in mathematics. Most of your post seems to be confused by trying to apply the usual meaning of "range" in ordinary colloquial English, and it's true that that meaning simply does not apply here.

"generic type" I assume refers to the part of concrete Rust syntax we usually call a type parameter, i.e. the T in fn foo<T>(t: T) { ... }. It's certainly true that this type parameter represents or describes a set of Rust types, in this case the set of all (sized) Rust types.

So if you squint at the original quote with all these synonyms in mind, its most literal meaning is just the tautology that a set of sets of Rust values is... a set of sets of Rust values.

What the quote is probably trying to do is point out that all these programming concepts, in their full generality, end up being equivalent or having similar relationships to each other that aren't obvious (or simply aren't true) from the perspective of any single concrete programming language. But I do think the exact intent of the quote is pretty muddled since it's unclear how concrete and how abstract they expect each word to be read. In other words, you're probably right to be confused by it (unless that quote had a lot of context you didn't link to).

EDIT: Oh, apparently this thread was split off TWiR quote of the week - #780 by L0uisc referring to Suggestion for making Rust's trait system more usable for generics - #68 by mankinskin I guess that doesn't change my post much though; I'm extremely unclear on what that thread was trying to propose.

4 Likes

When I first read this, range ~ set. It’s the only way it makes sense to me.

<T> on its own conveys “any type” (said differently, “all types”). That meets the definition of generic.

Only once we qualify <T> with a where clause do we start to specify how to identify “special” elements in the set of “any type”. E.g., types that implement the Display trait.

I can create a type struct Temp { value.. } with a smart constructor to enforce what it means to be temperature e.g., define a range of values. Note: Temp is a type (a set) that just so happens to specify a range that describes the elements in the set we called Temp.

Finally, note how the type signature tells us a lot about what fn can and can’t do (i.e., before having to dive into the body of function we know the scope of influence). If a function is generic for “any type”, the fn guarantees to never change the value of whatever <T> ends up being when made concrete at runtime. This is true because there are zero, none, nada transformations that can be implemented that works on “all types”. The only thing that we can do is id:: a -> a which returns the value unchanged. This is in fact what we do when implementing a generic fn for “all types*. Note, “all type” describes the “cargo” inside the container (or the like). The container is a concrete type, thus not generic.

In the fn that specifies a where clause, e.g., where T = Display, we now know something specific about the type that can be exploited in the body of the fn. While still generic, we moved from an infinitely sized type set (that includes types that implement any number of traits), to a subset with the elements that implement Display.

** in Rust, sometimes applying id on a value can have side effects that change the type of the container, or the like, but not the value itself. The best I can up with now is its like Vec<usize> -> &[usize]; no harm, no foul so still lives up to essence of the id promise.

2 Likes

Thanks for the clarification.

I'm glad you said that. It's not just me then :slight_smile:

That post conflate kinds and generic types.

While it is true that traits can be viewed as defining a range of types (i.e. you define a template of a sort from which types are made, similarly to how values are made from the templates we define with structs), that is at least somewhat misguided because at least in Rust traits cannot be used like types can.

For example, you cannot have local bindings to bare traits or have impl Trait fields.

With unized_locals you can have let x : Trait, but you can't use it normally (no moves)

Also you can have a trait as the last field of a struct

struct Foo {
    bar: Trait
}

The only issue is that Foo is now unsized and there is no safe way to construct it.

1 Like

My line of thought was basically, that a generic type parameter matches a set of types, defined by their trait bounds. Traits define a set of types, which a type can be part of. The word "ranges" was misused here, although I could imagine you could define an ordering on types, although probably not one that is generally useful. but in the same way types are only sets of values and not ranges of values, generally. It is just that values of the same type intrinsically have an ordering because of their bit representation, but actually that is not necessarily a useful ordering either, for example when storing memory addresses.
so actually both are just sets.

Generic types describe sets of types, just like types describe sets of values, and traits are to generic types what properties are to structs. So a new trait actually defines a new type of types, and can be combined with other traits to form a hierarchy of traits.

And the way that I meant that "traits are to generic types what properties are to structs" was under a few assumptions about generic types, which I am not sure are accurate. For one, I take generic types as compile time objects, used by the compiler to compile the code. And basically the compiler decides for generic code fn f<X: A>(x: X), whether a concrete type T can substitute the generic parameter by matching it against its trait bounds. So I imagined a generic type X: A + B + C with traits A,B,C to be like a struct X { A, B, C } and generic or meta types can be replaced by any type with all of those traits implemented. So in this image, the traits are like the properties of the generic type, which feels very intuitive for me.
And since you can also make traits depend on one another, they are kind of like generic types themselves. A trait X: A, B, C could be used as a trait bound to get the same behavior as above. So currently, the fn f<X: A, B, C>(x: X) syntax is actually like an inline trait, and I think it is equivalent to

trait X: A, B, C {};
fn f(x: impl X) {}

So if traits are meta types, what are their values? Their values are types. And a type is a combination of other types, with the most fundamental type being Bit, with values 0 or 1. Then a byte is 8 consecutive Bits in memory. Types are not unique, so we can have a type called ASCIIChar which is defined as a Byte, or u32 which is defined as 4 consecutive Bytes.

Reference types or pointer types are memory addresses, so they refer to a location in the one dimensional memory layout. They need to be interpreted correctly by the compiler so they work accordingly at runtime on the target machine.

Another type of type is a function, which is basically memory which is meant to be executed as a program. Unfortunately I have too little knowledge about compilers to understand how function types are actually implemented, but in functional programming they are values, which transform other values into new values. The function type A -> B -> C is the type of functions taking an A, then a B and producing a C. The value of this function type in the compilers memory should be something like the name of the function and the code of the function, which has to respect the structure of the input and output types.
So fundamentally, types are just meta-objects. Types which are used at compile time to build types.

Zig seems to have an approach like this. There, there is a meta-type type, whose values describe types. There you can build and manipulate types just like any other kind of values, as long as they are compile time evaluable. So types are just compile time constants describing types i.e. sets of values. and values are either data or programs (functions). Currently, in Rust, traits are to build types. They are like meta type properties, and anything with their property, e.g any type with this specific property, is in it's set of accepted substitutions.

If we also had data traits we could also build type sets which are narrowed by member fields, but this can be simulated already using traits, it's just a matter of syntax, as you could make a required trait function with the same name as the member field you want to match on during the generic compilation. But I think in this area Rust could actually encourage more general and natural APIs by improving the syntax around traits, which would make them easier to use and more powerful in meta-programming.

For example, I could see functions transforming types to other types being useful. I once had a use case where I wanted to have a version of a type where all of it's fields are Options. So I would have needed to write a macro which takes a struct definition and uses it to construct an additional type definition with all types wrapped in Option. This is still very error prone and very difficult to do, it would maybe be much more useful to refer to the field types of a type in a function and map them to an output type`s field types.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.