owned this note
owned this note
Published
Linked with GitHub
---
title: "RTN Analysis and Concerns - Josh"
tags: ["T-lang", "analysis"]
date: 2024-02-14
discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/RTN.20meeting.202024-02-16
url: https://hackmd.io/nbMtmAA2SsS5EbP3Omhwsg
---
# RTN Analysis and Concerns - Josh
## Summary
My concerns about RTN fall in two categories:
- Community impact and interface fragility/instability caused by the availability and use of RTN.
- The syntax of RTN not mirroring expression syntax, and being ambiguous in expression contexts, which closes off multiple future possibilities.
Based on further consideration, I no longer want to put forth an alternative to RTN.
Instead, I've proposed a set of mitigating mechanisms to address the former concern, and proposed specific requirements for potential RTN syntaxes to address the latter concern.
## Acknowledgement of the problem RTN solves
I very much appreciate the premise and value of being able to name the return value of an arbitrary trait method. I don't have any issue with that. I'm enthusiastic about more introspection mechanisms in general.
I like the idea of discouraging people from naming associated types when those types don't otherwise need a name, solely so that downstream users can write bounds on those associated types. I'm in favor of reducing the amount of "in case downstreams need it" boilerplate that library authors have to write, and that downstreams will push them to write if they don't.
And I'm a big fan of steps we can take towards "permissionless innovation", where we give people the capabilities needed to let them do innovative things that weren't anticipated by (for instance) their dependencies or other crates in the ecosystem.
## Potential community impact of RTN availability
Sometimes, providing a feature can steer the community in a particular direction, whether we intend that or not. There are some features that we've empirically observed people reaching for when they shouldn't; for instance, people who come from a language with global mutable variables reach for `static mut` when there are much better solutions. What we provide in Rust, how we present it, and how readily at hand it feels in different contexts, influences the ways people use Rust and the ways the Rust ecosystem evolves.
Consider the history of C++ generics. Historically (and still in widespread current practice), C++ generics effectively use compile-time [duck-typing](https://en.wikipedia.org/wiki/Duck_typing): your requirements on a C++ generic type are precisely what you do with it, without having to declare those requirements. Whether you can instantiate a template function depends on the body of the function, which makes abstraction and encapsulation difficult. Modern C++ introduced "concepts", which provide benefits similar to our traits, though much code still doesn't use them. Rust avoided that from the outset, by using only traits and not duck-typed generics.
I think RTN has a danger of devolving into "do duck-typing and then add RTN bounds until it compiles", rather than encouraging better interface design such as defining a trait or trait alias. We're talking about RTN in the context of Send bounds, but I think some developers learning Rust and trying to get their code to compile may end up writing bounds like `T::method(): PartialOrd + Debug`, which has many problems and seems like the wrong tool for the job. Most of the time, users should define a trait or a trait alias, and use that. Or, sometimes, they should be using a concrete type rather than a generic.
Using RTN in a function or data structure bound, rather than defining an interface, seems likely to make interfaces more fragile and less encapsulated, similar to C++ duck-typed generics. Once you've put RTN in the bounds of a public function, for instance, adding or removing a bound is likely to be a breaking change.
Proposed mitigating steps:
- **Ensure that we've defined and stabilized a trait alias mechanism before (or concurrently with) stabilizing RTN.** If we don't have a trait alias mechanism, people will have no option other than to inline their RTN usage on callers; having a trait alias mechanism lets us encourage people to use RTN primarily to define trait aliases.
- **Avoid adding any compiler suggestions that steer people towards RTN directly in bounds.**
- **Provide a lint against using RTN for bounds outside of a trait alias.** Make the lint warn-by-default for private uses, and deny-by-default for public interfaces, with suggestions about what to do instead, so that people are unlikely to *accidentally* create a fragile bespoke interface by using RTN in a function's or data structure's bounds. We can always change/relax this lint if people find other creative uses for RTN that we *don't* want to discourage.
- **Use trait aliases in our messaging and documentation about RTN.** Given the above, we'd need to use examples consistent with the usage we want to see.
Mitigations considered and rejected:
- Initially only permit RTN syntax in the same crate as the trait definition? This would sacrifice "permissionless innovation", but would encourage the definition and sharing of aliases like `ServiceSend`. I don't think this tradeoff is worth the cost, and it runs counter to the principles that (for instance) motivate relaxing the orphan rule.
## Syntax issue with the proposed RTN syntax
**I'd like to avoid bikeshedding syntax in this meeting.** Instead, I've proposed concrete requirements to apply to potential syntaxes, and I'd like to evaluate the requirement and then have *asynchronous* discussions about possible syntaxes that meet that requirement.
Writing `T::func(): Send` looks and feels wrong, in that the type syntax doesn't feel like it mirrors the expression syntax, not least of which because it uses nullary `()` no matter what arguments the function takes.
In addition to looking and feeling wrong, and throwing off people's mental parsers, this syntax also closes off some future possibilities that we may want:
- Uses of RTN in contexts that could be either a type or an expression. For instance, consider the case where `Type` is the return type of a function and you want to write `Type::CONST` or `Type::method(...)`. The proposed RTN syntax would be ambiguous with a function call.
- Uses of RTN that might depend on the parameter types of the function, or hypothetically in the future on the arity of the function (e.g. if we ever add safe varargs to Rust).
It's also less extensible to other things that we might want to introspect about a function other than its return type, such as its argument types or arity. For instance (straw syntax for illustration purposes), consider if we had `method::return` and `method::args::1` and `method::ARITY` and similar.
I would propose that we pick *some* syntax that has all of the following properties:
- Equally valid and unambiguous either in expression context *or* in type context
- Not ambiguous between "nullary" and "unspecified arguments"
- Ideally leaves room for future properties of a function
I don't want to bikeshed the actual syntax used; I'd just like a syntax that has these properties.