for trait<T>, Are trait<String> and trait<u8> two completely independent traits in the trait system?
Not really, e.g. you can't have
trait Trait<String>: Trait<f32> {}
Though sometimes I do think of it that way when dealing with bounds.
You can read this previous post for more information, but I personally do think of all trait
s as "trait
constructors"[1]. When there is no type parameter defined, the trait
is a "nullary trait
constructor", or perhaps more accurately a "unary trait
constructor" to account for the always-present Self
type parameter.
You don't need to pass in any type arguments into "normal" trait
s because there is only one possible argument you can pass; thus you don't/can't pass it. I think of this like "parameterless functions" which from a pedantic perspective don't actually exist[2], excluding the empty function; however when a function is defined on a domain of a single element, we tend to call this a "parameterless function" because there is only one possible argument to pass. Instead of forcing you to pass this argument, it's done "automatically". You can think of the domain as the unit type, and perhaps even read f()
as calling f
by passing in the instance of ()
. Functions that have parameters can then be viewed as passing in an n-ary tuple of the arguments (e.g., f(1u32, true, ())
is a case of calling function f
whose domain is the Cartesian product of u32
Ă— bool
Ă— ()
).
The reason you don't have the ability to explicitly pass Self
to any trait
constructor (including what I'm calling "unary trait
constructors") is we want to ensure that Self
actually represents the type that implements the trait
. Rust could I suppose allow one to pass in Self
explicitly, but for what benefit if that's the only thing you are allowed to pass?
The fact that you can't have trait Trait<String>: Trait<f32> {}
to me is not an indication that trait
s returned from Trait
are not completely independent, but a result that one cannot define a supertrait
relationship outside of the definition of the trait
constructor. To me,
trait Foo {}
trait Bar {}
trait Foo: Bar
is the same thing which does not compile since I've already defined the "unary trait
constructor" Foo
. trait Trait<T>: Trait<u32> {}
does not compile either, but that is because of something similar to "infinite recursion" not too dissimilar from an infinitely-recursive type.
Yes, I think of all types as type constructors as well where "normal" types are nullary type constructors/"parameterless" functions that return a type. ↩︎
If by "parameterless", you mean the domain of the function is truly empty; which is not the case for what one typically calls a "parameterless function". This is similar to how the unit type is not the type of "nothing" but is the type with exactly one member in contrast to an uninhabited type like
!
. ↩︎
Would you classify this as passing Self
to the trait constructor?
<SelfType as Trait<Param>>::trait-item....
I would not especially since I said "pass in Self
explicitly" (emphasis added). I suppose you can think of that as passing in Self
implicitly, but I find that confusing for two reasons:
- Syntactically it is clearly different than how you passed
Param
intoTrait
. - A similar construct does not exist in
impl
position; thus you'd at best be forced to accept that you can (implicitly) passSelf
in only certain situations.
I'd call that more path resolution. The compiler is the only "entity" that can ever pass in an argument for Self
into a trait
constructor; thus you have simply assisted the compiler in passing SelfType
.
I will also supply further "evidence" of why it's useful to unify trait
s with "trait
constructors", types with type constructors, and "normal" functions with polymorphic functions. The following code compiles:
/// Nullary-type constructor that is identical to the simpler
/// and idiomatic `struct Foo;`.
struct Foo<>;
/// Nullary (from the perspective of a developer)-`trait`
/// constructor that is identical to the simpler and idiomatic
/// `trait Bar {}`.
trait Bar<>{}
/// Passing in the "unit kind" into both `Bar` and `Foo`.
impl Bar<> for Foo<> {}
/// Why bother passing in the "unit kind" when this is easier?
impl Bar for u32 {}
/// Nullary-polymorphic function that is identical to the
/// simpler and idiomatic `fn fizz(){}`.
fn fizz<>(){}
fn buzz() {
// I'm passing in the "unit kind" (i.e., `<>`) into the
// nullary-type constructor `Foo`.
let foo = Foo::<>;
// Unlike "parameterless" functions, I'm allowed—and
// even encouraged for idiomatic reasons—to drop
// the "unit kind" (i.e., `<>`) altogether.
let foo2 = Foo;
// I'm passing in the "unit kind" (i.e., `<>`) into the
// nullary-polymorphic function `fizz`.
fizz::<>();
// Unlike "parameterless" functions, I'm allowed—and
// even encouraged for idiomatic reasons—to drop
// the "unit kind" (i.e., `<>`) altogether.
fizz();
}
/// I'm a nullary-type constructor just like `Foo`, but
/// why bother with the annoying `<>`?
struct Foo2;
/// I'm a nullary-`trait` constructor just like `Bar`, but
/// why bother with the annoying `<>`?
trait Bar2 {}
/// Even though I omitted `<>` from the definitions of `Bar2`
/// and `Foo2`, they're still there.
impl Bar2<> for Foo2<> {}
/// I'm a nullary-polymorphic function just like `fizz`, but
/// why bother with the annoying `<>`?
fn fizz2() {}
fn buzz2() {
// I'm passing in the "unit kind" (i.e., `<>`) into the
// nullary-type constructor `Foo2`.
let foo = Foo2::<>;
// Unlike "parameterless" functions, I'm allowed—and
// even encouraged for idiomatic reasons—to drop
// the "unit kind" (i.e., `<>`) altogether.
let foo2 = Foo2;
// I'm passing in the "unit kind" (i.e., `<>`) into the
// nullary-polymorphic function `fizz2`.
fizz2::<>();
// Unlike "parameterless" functions, I'm allowed—and
// even encouraged for idiomatic reasons—to drop
// the "unit kind" (i.e., `<>`) altogether.
fizz2();
}
The idea being that all of these things are just good old-fashioned functions like you've been taught in middle school! All that is different is that the domain of these "special" functions are types instead of instances of types, but in math anything can be a domain[1]! We say "type parameter" because they really are parameters of a function.
In fact in math one normally pronounces f(2) and f(x) as "f of 2" and "f of x" respectively. That mirrors how one normally pronounces something like Vec<u32>
: "vec of u32". Because Vec
, like f, is a function.
Due to it being idiomatic to drop <>
altogether for nullary functions who domain is a subset of types, things can get a little confusing since now it's not terribly clear what Foo2
above is: it is both a type constructor and a type. In contrast Foo
is more clearly a type constructor while Foo<>
is a type; however collisions like this are not uncommon. What is ()
? Is it the type ()
, or is it the lone instance that makes up the type? Hopefully it's clear from context.
Instead of using a generic term like "function", we use more specific terms like "type constructor". It's a function that "constructs" a type. A "trait
constructor" is a function that "constructs" a trait
. We don't typically call polymorphic functions a "function constructor", but I personally don't mind that terminology. Similarly an "instance constructor" is a function that constructs an instance of a type. Of course in Rust, "invoking" an instance constructor looks substantially different than invoking a "normal" function; but fundamentally it's a function.
Of course these functions that are defined on a domain of types are "invoked" by the compiler at compilation time in contrast to "normal" functions that are "invoked" at runtime—of course the compiler can sometimes optimize those functions such that they're "invoked" at compilation time too.
The last thing I'll add is that I'm guessing the reasons you can't omit ()
when defining and invoking a "parameterless" function like you can omit <>
is that it would "look weird" and f
already represents a function pointer so it's not clear what f
means: are you invoking the "parameterless" function or are you talking about a pointer to f
?
For the mathematicians out there, I know that's not technically true with Russell's paradox and all that jazz; but hopefully my intent is clear. ↩︎
I don't mind the mental model but disagree that there's an implicit parameter of unit type/kind.
In most formulations, mathematical functions can only take one argument (and functions like f(x, y) = x + y
are syntactic sugar for functions from ℝ² to ℝ). By contrast, Rust functions can take zero or more arguments, all of which are applied at the same time (there's no currying either). So, instead of being called with an implicit unit argument, it's just called with zero arguments. (After all, fn() -> ()
and fn(()) -> ()
are different signatures!)
That still works with the model you have above of "Rust functions/types are always parameterized by types", but it's a little smoother imo. (I don't know that it's the best mental model to use, but there aren't big problems with it.)
Also worth mentioning is that this doesn't unify type/trait constructors with types/traits - in fact, it's quite the opposite. This says that rather than u32
being a type, u32
is a nullary type constructor and u32::<>
is a type. After all, no matter what your mental model, Vec
is not a type because it's missing type arguments.
If you're interested in other ways to align programming functions with math functions, it might be interesting to learn about Hindley-Milner and System F, and typed lambda calculi in general (they all tend to treat polymorphism as proper functions from types to values). (In those systems, however, polymorphic functions/type constructors are definitively different from non-polymorphic functions/type constructors.)
On a separate note: as mentioned in the thread you linked earlier, Rust traits are comparable to Haskell typeclasses, which take Self
as an explicit first parameter. What wasn't pointed out there (that I saw) is that traits are kinda like structs that have a very deep integration with the rest of the language.
Well... that isn't really very true in Rust proper - traits can do a lot of things structs can't, like associated types and dyn
other languages
In other languages the connection is more obvious. My favorite example is Lean, where traits are literally just syntactic sugar over structs - the type system is expressive enough that anything that traits can do can be done by structs too (it just requires more typing).
. But I think it's a helpful reason to both show why Self
is a type parameter (just an implicit one) and to simplify your mental model of traits.
Consider this trait:
trait Foo {
fn foo(&self) -> u32;
fn bar() -> Self;
}
That declaration is kinda like saying this:
struct Foo<S> {
foo: fn(&S) -> u32,
bar: fn() -> S,
}
An implementation like this...
struct Baz(u32);
impl Foo for Baz {
fn foo(&self) -> u32 {
self.0
}
fn bar() -> Baz {
Baz(42)
}
}
// ...could be desugared like this:
// bless tells the compiler to implicitly use this constant whenever an instance of Foo<Baz> is needed
#[bless]
const fooBazImpl: Foo<Baz> = Foo {
foo: |this: &Baz| this.0,
bar: || Baz(42),
};
Whenever a function uses the trait, it works like an implicit const parameter:
fn qux<T: Foo>() -> u32 {
T::bar().foo()
}
// equivalent to...
fn qux<T, const F: Foo<T>>() -> u32 {
F.foo(&F.bar())
}
// when calling with T=Baz, the value of F is implicitly set to fooBazImpl because it's blessed
qux::<Baz, _>() // == 42
Anyway, that's one way to justify Self being a type parameter. Rust doesn't quite work like that, admittedly. But it's a fun mental model!
Edit: hit submit early cause I'm on mobile
I'm being "loose" when I say implicit parameter. What I was trying to enforce was that a function (in the math sense) is always defined on a non-empty domain[1], and some may incorrectly think that a "parameterless" function is one where the domain is empty especially since in many programming languages one does not actually pass any argument which can be hard to get your head around if the domain is not empty. This means one can read f()
as calling the function f
by passing in the nullary tuple/unit instance. Similarly f((),)
can be read as passing in the unary tuple containing the nullary tuple. This also aligns well with the typical math notion of a function since as you said functions are typically taught such that the domain is the Cartesian product of sets (i.e., you always pass in an n-ary tuple even if you prefer to think of the argument in a destructured form (e.g., a function whose domain is the algebraic field of real numbers is typically thought of as a function that takes in a real number and not a unary tuple containing a real number; however there is a trivial bijection that can take you from one model to the other)).
Of course this only "looks" good when you use a language where the lone instance of the unit type/nullary tuple is represented as ()
. If a language represents such an instance differently but still retains the same function syntax, then this mental model starts to fall apart/is harder to justify. Technically even in Rust this mental model is a little weird when dealing with a unary function since an instance of a unary tuple almost always requires a trailing ,
; but in this mental model, only f(1u32,)
should work when f
is a unary function but f(1u32)
also compiles illustrating that there is still something missing by reading the previous invocation as calling f
by passing in the unary tuple containing 1u32
.
The point of this mental model is not to be used as a substitution for proper type theory (e.g., homotopy type theory) but as what I view as a pretty simple model that can take one reasonably far even in "esoteric" languages like Haskell.
Similarly my use of the phrase "unify trait
s with trait
constructors" was a little careless as I hoped the rest of the post made clear, but clearly I failed in that regard. I wasn't saying types and type constructors are the same but that what one sometimes calls a type is actually a type constructor in a certain context; hence my examples of Foo
and Foo2
.
Additionally this mental model tends to work pretty well with polymorphic functions where you have a function from types to functions instead of types to types like a type constructor.
If this mental model doesn't do you well; then yes, please throw it away and try to use a different one. Certainly proper type theory or a formal education in intuitionistic logic will take you further in the field of programming language theory than "typical" math based on classical logic; but for those whose background is more on the classical logic side of things, I have found this mental model immensely useful at least as a stopgap for a proper type-theoretic model.
I intentionally did not try to go deeper into what a trait
is due to complex things like associated types; furthermore I prefer to think about trait
s as a way to express the "structure" of a type even though this is obviously extremely informal. So I actually don't like thinking about trait
s like struct
s as you have attempted to explain, but that certainly doesn't make it wrong for someone to have such a mental model.
Again ignoring the empty function (i.e., "the" function whose domain is the empty set). ↩︎