[rust-dev] Type system thoughts

Gábor Lehel illissius at gmail.com
Fri Nov 15 08:05:28 PST 2013


Hello list,

I have some ideas about typey things and I'm going to write them down. It
will be long.




It would be nice if `Trait1 + Trait2` were itself a trait, legal in the
same positions as any trait. This is already partly true: in trait bounds
on type parameters and super-traits of traits. Where it's not true is trait
objects, e.g. `~(ToStr + Send)`. Having this could remove the need for the
current `~Trait:OtherTraits` special syntax.

I wonder whether lifetimes could also be interpreted as traits, with the
meaning: "[object of type implementing lifetime-trait] does not outlive
[the given lifetime]". This is an honest wondering: I'm not sure if it
makes sense. If it does make sense, it would fit in perfectly with the fact
that 'static is already a trait. Together with the above, it might also
allow a solution for capturing borrowed data in a trait object: you could
write `~(Trait + 'a)`.




The next few are going to be about higher- (or just different-) kinded
generics. To avoid confusion, I'm going to use "built-in trait" to mean
things like `Freeze` and `Send`, and "kind" to mean what it does everywhere
else in the non-Rustic world.

I think the best available syntax for annotating the kinds of types would
be to borrow the same or similar syntax as used to declare them, with
either `type` or `struct` being the kind of "normal" non-generic types (the
only kind of type that current Rust lets you abstract over). [I prefer
`type`, because both structs and enums inhabit the same kind.] This is kind
of like how C++ does it. For example, the kind of `Result` would be
`type<type, type>`. Our `type` corresponds to C++'s `typename` and
Haskell's `*`, and `type<type, type>` to C++'s `template<typename,
typename> class` and Haskell's `* -> * -> *`. So, for example, you could
write the fully kind-annotated signature of an identity function restricted
to Result-shaped types (yeah, actually doing this would be pointless!) as:

    fn dumb_id<type<type, type> R, type A, type B>(x: R<A, B>) -> R<A, B>;

To explicitly annotate the kind of the Self-type of a trait, we could
borrow the `for` syntax used in `impl`s. Here's the fully kind-annotated
version of the `Functor` trait familiar from Haskell:

    trait Functor for type<type> Self {
        fn fmap<type A, type B>(a: &Self<A>, f: |&A| -> B) -> Self<B>;
    }

(Obviously, explicitly annotating every kind would be tiresome, and `Self`
is a little redundant when nothing else could go there. I could imagine
`trait Functor for type<type>`, `trait Functor for Self`, and/or `trait
Functor` all being legal formulations of the above. I'll get back to this
later.)

One question with regards to higher-kinded traits is what to do about
methods and `self`. What syntax to use? Does it even make sense? For
syntax, I see two possibilities (snipping from the `fmap` signature):
 - `self: &Self<A>`
 - `&self<A>`
I don't have a preference, but I'll be using the latter for now. And it
*does* make sense. Every trait over a `Self` with parameters of given kinds
gives rise to trait objects with parameters of the same kinds. Basically,
it's always the outermost type constructor that gets erased. In the trivial
case, the `Self` of `ToStr` does not have any parameters, and neither does
`~ToStr`. To see a less trivial case, let's write a variant of Haskell's
`Foldable`:

    trait Foldable for type<type> Self {
        fn fold<T>(&self<T>, init: &T, folder: |&T, &T| -> T) -> T;
    }

Then `&Foldable<int>` is a trait object hiding any foldable container of
ints (for example `~[int]`). There should probably be a way to distinguish
the arguments of the trait from the arguments of Self: so perhaps that
should be something like `&Foldable<for int>` instead (but that doesn't
cleanly extend to more than one argument). An example of its use:

    fn sum(vals: &Foldable<for int>) -> int {
        vals.fold(&0, |a, b| a + b)
    }
    let n = sum(&[1, 2, 3] as &Foldable<for int>);

So far we've seen two sorts of kinds: the base kind `type`, and generic
kinds `type<...>`. Another very important one is lifetimes. And I can
imagine two more beyond these:

Like C++, types could be parameterized over constants. Again, the syntax
could mirror their declarations. For example, a function to construct a
fixed-length array:

    fn make_n<static N: int>(n: int) -> ~[int, ..N] { [n, ..N] }

Interesting questions here include what types to allow, how or whether to
handle literals of various types as type arguments, and the constant
expression sublanguage.

The other is the potential to abstract over traits. GHC does this with the
`ConstraintKinds` extension. (According to people who have implemented
Haskell compilers, it's actually easier to implement it than not to![1]
Which is pretty crazy.) In that case, just to stick to trivial, useless
examples, and continue with parameter-syntax-follows-declaration, you could
write a generic alias for `Trait1 + Trait2` as:

    trait And<trait A, trait B>: A + B { }
    impl<trait A, trait B, type T: A + B> And<A, B> for T { }

[1]:
http://www.reddit.com/r/haskell/comments/10w3cx/ideas_for_a_library_that_uses_constraintkinds_to/c6h6di4

(I don't want to dive any more deeply into the motivations for and
complications of this feature here; I only wanted to mention the
possibility and demo the syntax.)

There's two further issues that higher-kinded generics raise. The first is
that in order for traits like `Functor` to be truly useful, you really want
currying and partial application at the type level. Otherwise, while you
can `impl Functor for` arrays and `Option`, you can't impl it for `Result`
and `HashMap`, which is a significant loss. Thankfully, there are no
difficulties regarding evaluation order at the type level, which is one of
the reasons that partial application syntax was removed at the value level.
I could imagine the syntax being something like this:

    impl<K> Functor for HashMap<K, ..> { fn fmap etc. }

in which case `HashMap` is partially applied to `K`, and the remaining
parameters (only one in this case) are left hanging - and so the result is
of the `type<type>` kind that `Functor` expects (rather than the
`type<type, type>` kind of plain `HashMap`). What the above means is that
if you have a `HashMap`, you can make a new `HashMap` with the same keys
but different values of a potentially different type.

I expect that kind annotations would be legal anywhere a type parameter is
declared. The second issue is: in which cases would they be optional, in
which cases required, and what happens if you omit one? I see several
possibilities:

  - If you omit a kind, it defaults to `type`. Any other kind must be
explicitly annotated. This is the simplest and dumbest option, and is
completely backwards compatible.

  - If you omit a kind, it is inferred. If it can't be inferred, an error
is raised. This is not quite backwards compatible.

  - If you omit a kind, it is inferred. If it can't be inferred, it
defaults to `type`. This is again backwards compatible with current Rust.

  - If you omit a kind, it is inferred. If it is unconstrained, the
declaration is kind-polymorphic, meaning it works with any kind. I think
this is backwards compatible, but I'm not 100% sure. GHC has this with
`PolyKinds`. The absence of it is frequently annoying in C++ when writing
template-heavy code: you have to manually duplicate things for `typename`,
`template<...> class`, various non-type template parameters, and all the
combinations of these that you want, and it's impossible to get full
coverage with a finite number of declarations: you have to choose the ones
you want to support. Proposals to address this issue have been submitted to
the C++ standards committee. Similarly, in Haskell's case, they were forced
to have `Typeable1` ... `Typeable7` classes (the equivalent of our `Any`
trait) for kinds of a few selected arities, which will be collapsed into a
single `Typeable` in the next GHC, both reducing duplication and attaining
full coverage of every type of every kind.




Higher-kinded types are connected to dynamically sized types. In
particular, I think sized and unsized types should be *separate kinds*,
instead of using a built-in trait to distinguish them. Multiple
considerations support this:

 - They are qualitatively different. Built-in traits are required only for
safety. You could imagine leaving one off, and it would still make sense,
it just wouldn't be safe. On the other hand, an unsized type in the wrong
place is close to nonsensical. Defining what kinds of types make sense in
what places is exactly what kinds are for. `[[int]]` doesn't make sense in
a similar way to how `let foo: HashMap`, `[Option]`, and `fn foo<T: int>`
don't make sense. In fact, this is precisely what GHC does: it has separate
kinds for boxed types (`*`), unboxed types (`#`), and trait bounds which in
Haskell are called constraints (`Constraint`).

 - If `Sized` is a built-in trait, it re-raises questions about whether
trait bounds should be allowed on struct type parameters. If the
distinction is made at the kind level, this kind of problem goes away: type
parameters already have kinds.

 - A `Sized` trait significantly increases the annotation burden. If the
distinction is made with kinds, on the other hand, they could be inferred
with the same kind inference algorithm used for all other kinds (supposing
support for different kinds and kind inference exists).

That still leaves the question of what the kind hierarchy should
specifically be. A first attempt might look something like this:

    type { sized, unsized { array, trait } }

where kinds inside braces are subkinds of those outside. (Even GHC has
subkinds, so I hope subkinding is not a big problem.) I *think* that mostly
makes sense: the type parameter of an array would have `sized` kind; the
type parameter of a pointer type would have the most general `type` kind,
but its representation would be different for `sized` and `unsized`; and in
trait-bound positions things of `trait` kind would be accepted, but not
arrays. I haven't put much thought into the actual names and syntax for all
of these. (In particular, I don't think there's an actual use case for
specifying `unsized` or `array` kinds: they're just what you get if you
take the complement of `sized` and `trait`, respectively, relative to their
superkind.)

This gets more interesting if we also have struct inheritance, and structs
can be used as traits (with the meaning is-a-substruct-of). I haven't
thought about that possibility very much yet.




The next few are about generics and dictionaries (vtables). I'm at a
somewhat earlier stage in my thinking about this than about the other
things.

Current Rust has two things it can do with generics, traits and their
dictionaries:

 - In the case of generic functions, traits are completely monomorphized
away: the implementations to call are all selected at compile time;

 - Rust also has syntactic sugar for a form of existential types in the
form of trait objects, in which case a pointer to the trait's dictionary is
packaged along with a pointer to the object, and the implementation to call
is determined at runtime.

I think there are a few other directions that could be explored. In recent
blog posts[2][3] Niko Matsakis mentioned the possibility for closures which
are generic over a lifetime (not top-level functions, but closures!) --
meaning that the lifetime(s) the closure will be invoked with are not known
until runtime. This works because the representation of the closure does
not depend on the lifetime: that information is completely erased during
compilation. The basic idea in all of the following is that lifetimes
aren't the only thing which don't affect representation: neither do the
type arguments to pointer-like types! [Let's pretend for now that only
sized types exist.] This opens the possibility of generic closures
parameterized over *types*, provided those types are only used as arguments
to pointer-like types. This corresponds to pointers to function templates
in C++ (which are notable for being occasionally desired but impossible),
and to higher-rank types in Haskell. For example, a generic closure
representing the identity function restricted to owned pointers would have
the type:

    for<T> |x: ~T| -> ~T

Here I'm using our ever-versatile `for` keyword to introduce the type
parameter list, which I think reads better than the plain `<T> |x: ~T|`
style from Niko's blog posts (and tracks Haskell's use of `forall` for the
same purpose). So far so good: a closure of the above type will indeed have
the same representation no matter what type it's used with. But there's a
wrinkle: what if there are trait bounds on the type parameter? Then the
closure will have to call different trait methods depending on which type
is used, seeming to require different representations for each. There's a
way out: pass a pointer to the trait dictionary (vtable) as a hidden
argument to the closure, and make calls to trait methods (and associated
functions) through that pointer. This is analogous to how pointers have a
hidden pointer-to-vtable field when they are pointing to trait objects.
(Incidentally, the fact that pointers have the same representation
regardless of the type they are pointing to is also what allows
pointers-to-trait-objects to work at all.) In both cases, this happens to
be the same thing that GHC does. There are few more interesting questions
and wrinkles this raises:

 - Looking at the above generic closure, what if it did not return its
argument, but let it go out of scope? Then it would have to invoke the
destructor for the given type - which again varies depending on which type
it is! This points back to the fact `Drop` really is an honest-to-god
trait, and it's not just a superficial syntactic resemblence. The reason
`Drop` is so weird is that, in effect, there's an implicit `impl` of it for
every type (when there isn't an explicit one), which does nothing, and it's
present as an implicit trait bound on every type argument. (You could
imagine it working like any other trait, and requiring an explicit `Drop`
bound in any generic function where an object of that type is destroyed
(transitively), but that would be super-annoying which is presumably why we
don't do it.) In any case, to make this example work, we would have to pass
a pointer to the `Drop` dictionary for the type as a hidden argument to the
closure, the same as with other traits. (This is similar to the issues with
owned pointers and struct inheritance from another of Niko's blog posts[4],
though I'm not sure what it teaches us.)

[2]:
http://smallcultfollowing.com/babysteps/blog/2013/10/29/intermingled-parameter-lists/
[3]:
http://smallcultfollowing.com/babysteps/blog/2013/11/04/intermingled-parameter-lists/
[4]:
http://smallcultfollowing.com/babysteps/blog/2013/10/24/single-inheritance/

 - There's the question of how to actually codify which generic function
types are legal and which ones aren't. For example, `for<T> |x: T| -> T`
and anything else which uses a generic argument unboxed should not be
legal. This feels like another thing that the kind system should handle,
but I'm less sure about exactly how. At first, there's a seductive
possibility: what if all types introduced in a `for<>` were considered
unsized? In a sense this is literally true: because the type is not known
until runtime, neither is its size, and unsized types can only be
manipulated through pointers, which is what we want. I'm not sure if it
completely works out, though. For one thing, unlike "normal"
pointers-to-unsized types, here pointers to these "pseudo-unsized" types
are still only a single word, they don't have hidden pointer-to-dictionary
fields (which are passed as hidden arguments to the closure, instead).
Perhaps if the sizedness of a type were treated separately from whether it
carries a dictionary, this could be made to work out (for example, `Send`
and `Freeze` don't have dictionaries either), but it's not clear how that
might be done. The other issue is that this seems too restrictive for the
return types of functions. Rust writes out the return value of a function
through a hidden pointer that is passed in to the function, so it seems
like it should be possible to have closures which return a generic value
"unboxed": while the closure does not know what type it is, the caller
does, so the caller can pass in a pointer to an appropriately-sized piece
of memory, and it would seem to all work out. For example, a type like
`for<T: Clone> |x: &T| -> T` would then be legal. And because values are
implicitly returned through pointers, there's no penalty to returning
values "unboxed", and so most Rust function do that, even if they take
their arguments by pointer: it would be a shame to exclude them all.

 - Earlier I said "let's pretend for now that only sized types exist". It's
obvious why: the size of pointers can change depending on the *kind* of the
type they point to, which would invalidate the property we've been relying
on. In can imagine two remedies. One is in fact to simply restrict the type
parameters of generic closures to sized kinds. The other is to permit
different kinds of the programmer's choice, as long as they are used
consistently and not mixed. For example, `for<sized T> |x: ~T| -> ~T` and
`for<trait T> |x: ~T| -> ~T` would both be legal under this scheme, and
incompatible.

Let's look at a few more fun things you could do using similar tricks as we
used for generic closures. One is another classic example of something you
can't do in C++: virtual function templates. In Rust terms, this
corresponds to calling a generic method through a trait object pointer.
It's clear why C++ disallows this: function templates work by generating a
separate actual function for each instantiation of the template
(monomorphization), and virtual functions work by calling the function
through a pointer, and you definitely can't have a single pointer to an
arbitrary number of different functions template instantiations. Rust could
do mostly the same thing here as with generic closures: if the generic
arguments of the method are only manipulated through pointers, then
generate an additional "generic" instantion of the method where trait
dictionaries are passed as hidden arguments, and when calling the method
through a trait object, use this instantiation of it and pass in the
dictionaries. After that, the same considerations apply as for generic
closures, from above.

Another possibility is explicit existential types inside of structs. That
might look like this:

    struct LeftFold<In, Out, 'a> {
        type Seed,
        initial: &'a Seed,
        folder: |&Seed, &In|:'a -> Seed,
        result: |&Seed|:'a -> Out
    }

(This is borrowed from the blog post "Beautiful Folding" by "Quiz"[5].)
This represents a type which encapsulates and hides both the folding
function and accumulator of a left fold. You pass in values of type `In`,
it does the folding work behind the scenes, and you get out a value of type
`Out`. (See the blog post for more details). Again, the same restrictions
apply here to the use of the `Seed` type as to generic arguments of generic
closures and methods. (If you were to use it unboxed, the struct could not
be represented.) Another thing we could do with this, along with the
`trait` kind from earlier, is write out the representation of a trait
object pointer explicitly:

    trait BorrowedObject<'a, trait Trait> {
        type T: Trait,
        object: &'a T
    }

Here, the pointer-to-Trait-dictionary will be stored as a hidden field at
the end of the struct. This results in the same representation as for the
trait object pointers that are built-in to the language. (The `object`
field itself is only one word wide, the same as pointers to generic
arguments of generic closures.)

[5]: http://squing.blogspot.hu/2008/11/beautiful-folding.html

Finally, one more thing that could be supported in this way is polymorphic
recursion. The problem with polymorphic recursion in monomorphizing
languages is that it requires an infinite number of "template
instantiations" as the recursion descends, but if the representation of the
functions is the same no matter what type arguments they are used with,
then the problem goes away. Again, the same restrictions would apply as for
all of the previous examples.




The last few are about closures. I really like the observation originally
made by strcat (Daniel Micay) that closures are basically just trait
objects[6]. In effect, our current (recently introduced) closure type
corresponds to this (using much imaginary syntax):

    trait FnMut<Args..., Ret> {
        fn call(&mut self, Args...) -> Ret;
    }

    type |Args...|:'a -> Ret = &'a mut FnMut<Args..., Ret>;

Looking at it this way, it makes sense why these closures are move-only:
they mutate their environment, hence `&mut self`, which requires the
pointer to the trait object to also be `&mut`, which is move-only. (An
aside: wouldn't `&mut 'a T` perhaps be nicer than `&'a mut T`?) Our
recently introduced `proc` type corresponds to this:

    trait FnOnce<Args..., Ret> {
        fn call(self, Args...) -> Ret;
    }

    type proc(Args...) -> Ret = ~FnOnce<Args..., Ret>;

Finally, there's a third important possibility (the old `&fn`) which
neither of the above capture:

    trait Fn<Args..., Ret> {
        fn call(&self, Args...) -> Ret;
    }

    type &'a fn(Args...) -> Ret = &'a Fn<Args..., Ret>;

This corresponds to closures with an immutable environment, which can
therefore be freely copied. In addition, if you mix and match the traits
and pointer types, `~FnMut<Args..., Ret>` is also a useful point in the
space, allowing mutation of the owned environment; and so is `@Fn<Args...,
Ret>`, which is basically the type of closures in Haskell (if you don't
consider laziness), allowing arbitrary sharing of immutable environments
between closures.

[6]: https://github.com/mozilla/rust/issues/8622

The reason all of this is so cool is that it unifies the C++ practice of
using unboxed closures via an overloaded `operator ()` with the standard in
functional languages of having a single first-class function (boxed
closure) type. Like all other templates-plus-overloading in C++,
overloading the function call operator corresponds in Rust to using a
trait. Then if you add trait objects, boxed closures - the equivalent of
C++'s `std::function` - fall out for free! (With memory management of the
closure chosen by the client, unlike `std::function`.) Like C++, you could
`impl` these Fn traits for your own types, and like functional languages,
you have the liberty of using a single unified function type (boxed
closures). (Or, well, three, thanks to Rust's distinction between by-ref,
by-mut-ref, and by-val: but they're effectively subtypes of each other, in
the reverse order.)

The problem is that doing all of this by hand in Rust with user-defined
traits is super-awkward. There's no built-in support for variadic
arguments, so you have to simulate it with tuples, or define separate
`Fn0`, `Fn1`, .. `FnN` traits; you don't have literals, so you have to
write the closure `struct` by hand and `impl` the trait for it (just like
C++ before lambdas); you can't make stack closures with the stack frame
itself as the environment; and you don't get to use the function call
operator, but have to call the `call()` method. It would be nice if Rust
solved these by in fact overloading the function call operator on traits
such as these; had some kind of built-in syntax for the traits, to solve
the variadic and plain ugliness problems (my old idea was built-in
special-syntax `trait fn(T...) -> U`, `trait fn mut(T...) -> U`, and `trait
fn once(T...) -> U` at each arity, like tuples); and had closure literals
desugar to anonymous structs `impl`ing the appropriate trait (similarly to
C++ lambdas). Perhaps the current closure types could then also desugar to
the afore-mentioned things.




If you got this far: thanks for reading! (My condolences.) I hope any of
this proves to be useful in any way. I'd be happy to elaborate on anything
if anyone wants me to elaborate on it.

-Gábor


-- 
Your ship was destroyed in a monadic eruption.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/rust-dev/attachments/20131115/622f94b4/attachment-0001.html>


More information about the Rust-dev mailing list