-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Efficient code reuse #349
Comments
Comment moved to discuss. |
What ended up happening? :) |
That paragraph is unfortunately outdated: we'd actually already postponed all discussion to after 1.0 since we believe any changes/features we will add are backwards compatible and there's a lot of more urgent (I.e. backwards incompatible) work that took priority. |
I understand. I'm excited to see what ends up being implemented. |
One thing I love about rust is that its non-free abstractions are usually explicit. I love knowing that a struct is "just a struct". It would be nice to say that features requiring runtime and fancy "under the hood" data representation support (for some value of fancy) are built from pluggable, orthogonal components. So how about something like "anonymous composition is inheritance"? The idea would be that
implies a specific struct layout with the requisite syntactic sugar. For simplicity let's suppose that we only allow one anonymous component per struct. EDIT: It was pointed out to me by @steveklabnik and others that rust has had trait objects since forever. I'll keep the following bit in place and just say that std::raw::TraitObject is the kind of thing I would love to be not only "pluggable" but "leave-out-able", in the sense that if I'm writing an OS and I can't implement that yet, I can tell the compiler that it can't make me any TraitObjects and that it wouldn't be ok for me to use them right now. For dispatch, it would be nice if we could plug-in and compose the dispatch resolution mechanism. I don't want the "doesn't play well with others" feel of C++ vtables. What if I want to back this part of the language with the Objective-C runtime? Or Glib? The current "personality" of rust feels like this should be possible, in the same way that "give me your allocator function and then I'll let you use boxed things" works. I guess my main point is that rust is the first language in a long time where I really feel like the modern features of the language don't come with being chained to a runtime layout and functionality set that's given from up on high by the Gods of Rust. I would love it if the rust team could implement functionality like this while still retaining that ethos. |
Virtual dispatch will of course be explicit under this proposal or any other. |
It's worth mentioning that @nikomatsakis has been working on this proposal: http://smallcultfollowing.com/babysteps/blog/2015/08/20/virtual-structs-part-3-bringing-enums-and-structs-together/ |
Replaced `list` with `iter`, since that is the function arguments' name
What's the state in 2018? Couldn't we agree to any "inheritance" proposal? |
What happened to @nikomatsakis's proposal? I'm trying to create a general UI framework for Rust, basically based on push-pull FRP. There's no way to do it, however, because there's no way to model a type hierarchy: enums don't actually "add" types together, and the nodes of enums aren't really types, only tags. |
Now that rust 2018 edition happened, shouldn't the priority be bumped up? This is my main limiting factor and probably the last big remaining reason for outsiders to not learn rust... |
It's been five years. Isn't it about time this is un-postponed? |
Well, if you insist on composition... struct Animal {
pub name: String,
}
impl Animal {
pub fn print_name(&self) {
println!("{}", self.name);
}
}
struct Dog {
owner: String,
animal: Animal,
}
impl std::ops::Deref for Dog {
type Target = Animal;
fn deref(&self) -> &Self::Target { &self.animal }
}
fn main() {
let dog = Dog {
owner: "John".into(),
animal: Animal { name: "Ripley".into() },
};
dog.print_name();
println!("{}, {}", dog.name, dog.owner);
} |
@aldanor Using |
From what I remember, last it was discussed at length, the result was "Desirable, but not as easy as it sounds and we have things that are needed more urgently, like async/await, to get implemented first". (i.e. There's no single objectively superior way to implement a code reuse mechanism, so more discussion is needed to decide which trade-offs are best to enshrine in the core language as the blessed primary way to do it.) |
There was a consensus that delegation should be explored outside rustc first, ala delegate. We've enough plausible delegation patterns that practical exploration helps. I suppose doing delegation via proc macros could expose more proc macro shortcomings too, which then improves proc macros and better supports whatever complex delegation scenarios rustc excludes. I frequently flip the type structure wherever I'd otherwise be tempted by simple local inheritance:
vs
Any methods you want inherited from |
I think this way is the best workaround. But still, that working out this issue by |
I wonder if properties (which would be allowed in traits) would be a good option. I'm quite fond of them, but I've heard opposition to them for being too magical. I disagree, but oh well. |
What if Animal is defined in another crate, and Dog is defined in your crate. And some point down the line, Animal adds a field Dog already has. I think this gets into the same kind of thinking around the current orphan rules and coherence. |
@dbsxdbsx @ibraheemdev I personally would implement that like this: struct Animal {
pub name: String,
}
trait AnimalExt {
fn get_animal(&self) -> &Animal;
fn get_name(&self) -> &str {
&self.get_animal().name
}
}
impl AnimalExt for Animal {
fn get_animal(&self) -> &Animal {
self
}
}
struct Dog {
owner: String,
animal: Animal,
}
impl AnimalExt for Dog {
fn get_animal(&self) -> &Animal {
&self.animal
}
}
fn main() {
let dog = Dog {
owner: "John".into(),
animal: Animal { name: "Ripley".into() }
};
println!("{}", dog.get_name());
println!("{}, {}", dog.get_name(), dog.owner);
} I think it is more scalable for the number of methods and does not abuse |
While many people suggest abusing Deref for code reuse, it is actually both ergonomic and idiomatic to use fn feed(mut animal: impl AsMut<Animal>) {
animal.as_mut().food += 1;
}
struct Dog {
animal: Animal,
other_fields: Xxx,
}
impl AsMut<Animal> for Dog {
fn as_mut(&mut self) -> &mut Animal { &mut self.animal }
}
let dog: Dog = unimplemented!();
feed(&mut dog); This problem is already well-solved with |
@SOF3 This is a nice none-hacky approach but how do you solve wanting to let base = get_some_subtype();
match base {
SubType1 => ...
SubType2 => ...
} This is why I want code reuse in the first place. I want to be able to fully access info about both the base and sub types, especially to match on base types and do branching logic (no matter whether that is implemented through inheritance or composition). |
What is the type of |
By matching on a ref (by having a discriminant of some kind similar to enums) in some way maybe. The snippet above isn't rust sanitized and haven't thought it through much. What I mean is I've been wanting a way to achieve that as a result (not through enums, but through inherited/composited types), disregarding the method/syntax, and we don't have that yet. |
that is specialization, which is an antipattern. why not use enums for that? alternatively, make it a method in Animal/AminalExt? although unidiomatic, this can also be achieved through Any::downcast_mut() in an if-let-else-if-let chain. |
Enums don't really solve all cases relating to this. Just look at how rustc tries really hard to emulate inheritance by using enums for the different nodes in the tree (and one of the members talked exactly about this case when they were discussing specialization in the past). Enums just are not always the answer. Whether it's called specialization or something else, rust is in serious need for a better way to compose shared code, is what I'm saying. Some workarounds work for the simpler cases (enums, deref, AsRef), but they're not nearly enough. |
I am not sure what the problem encountered in rustc is, but last time i had to work on a flow analysis in Java, I really wished they were enums. |
@mrahhal I made a comment on Reddit about this issue not too long ago. In short, it's not related to struct inheritance, but the fact you can't downcast any-old trait object (e.g. |
@Iron-E Thanks, I didn't know about this. But it's not related to the code reuse issue. Traits unify signatures and are not the answer for code reuse. I'm not saying it should be related to inheritance either, the solution could very well be through composition. In summary, need a code reuse solution (won't repeat what this issue lists as requirements) that also provides the ability to pattern match on type kinds (a type kind's meaning is an impl detail). (But thanks for the downcast pointer, might be useful in certain cases) It's unfortunate that this has been in stagnation since 2014. Rust is great, but this is one of the biggest blockers if you're modeling certain complex logic that wants some kind of code reuse, without having to resort to an amalgamation mess of unreadable macros. |
I still don't see what's missing with enums. Code reuse through enum delegation could be convenient given the appropriate codegen mechanisms (such as a decl macro derived from a proc macro). |
One example is when you want to restrict types. Imagine building a tree model for a compiler and having specific nodes (IfStatementNode, UnaryExpressionNode, etc) representing the structure, you want a few base node types that reuse some logic, and then you want to store specific node types in other nodes (or simply accept certain types in functions). You can't do any of this in rust today. (yes, I know I can nest structs inside of each other, but people reaching out to Deref hacks is proof this isn't a valid solution for many cases) The way rustc and servo work around this is by having multiple enums (Statement, Expression, etc) and compromising by restricting to the common denominator enum (Expression for example), but this results in a lot of annoyances throughout the code (it's not strong typed). The problem is not specific to wanting to model intrinsically hierarchical models, but it is a pain there in particular. And as I just noted above, I don't want macros to solve common reusability needs. |
@mrahhal a partial solution for this would be combining enum UnaryExpr {
A, B,
}
enum BinaryExpr {
X, Y,
}
#[inherit(UnaryExpr, BinaryExpr)]
enum Expr {}
// `Expr` contains variants `UnaryExpr`, `BinaryExpr` |
@zseri I've explored many solutions including this. It's still far from what I want. Yes you can easily create a visitor that exposes a "flattened" match on the type, but that alone doesn't solve it. Even if Right now I implement something similar to what rustc does, multiple enums and a few macros to handle some of the logic reuse (no tool can semantically parse macros so somehow we're back to using |
我认为写一个完整一点的例子比较起来比较直观, @ibraheemdev @Iron-E @mrahhal @zseri 以下是golang代码通过结构体内嵌实现的属性和方法复用代码方式,这是golang面向对象的处理方式,确实很方便。怎么转换到 rust的 trait方式复用代码呢? impl BaseRequestExt for SendSmsRequest {
fn base(&self) -> &BaseRequest {
self.rpcRequest.base()
}
fn base_as_mut(&mut self) -> &mut BaseRequest {
self.rpcRequest.base_as_mut()
}
} 详细的对照代码如下:
// base class
type baseRequest struct {
Scheme string
Method string
Domain string
Port string
RegionId string
ReadTimeout time.Duration
ConnectTimeout time.Duration
isInsecure *bool
userAgent map[string]string
product string
version string
actionName string
AcceptFormat string
QueryParams map[string]string
Headers map[string]string
FormParams map[string]string
Content []byte
locationServiceCode string
locationEndpointType string
queries string
stringToSign string
}
func (request *baseRequest) GetQueryParams() map[string]string {
return request.QueryParams
}
func (request *baseRequest) GetFormParams() map[string]string {
return request.FormParams
}
func (request *baseRequest) GetReadTimeout() time.Duration {
return request.ReadTimeout
}
func (request *baseRequest) GetConnectTimeout() time.Duration {
return request.ConnectTimeout
}
func (request *baseRequest) SetReadTimeout(readTimeout time.Duration) {
request.ReadTimeout = readTimeout
}
func (request *baseRequest) SetConnectTimeout(connectTimeout time.Duration) {
request.ConnectTimeout = connectTimeout
}
func (request *baseRequest) GetHTTPSInsecure() *bool {
return request.isInsecure
}
func (request *baseRequest) SetHTTPSInsecure(isInsecure bool) {
request.isInsecure = &isInsecure
}
func (request *baseRequest) GetContent() []byte {
return request.Content
}
func (request *baseRequest) SetVersion(version string) {
request.version = version
}
func (request *baseRequest) GetVersion() string {
return request.version
}
func (request *baseRequest) GetActionName() string {
return request.actionName
}
func (request *baseRequest) SetContent(content []byte) {
request.Content = content
}
func (request *baseRequest) GetUserAgent() map[string]string {
return request.userAgent
}
func (request *baseRequest) AppendUserAgent(key, value string) {
newkey := true
if request.userAgent == nil {
request.userAgent = make(map[string]string)
}
if strings.ToLower(key) != "core" && strings.ToLower(key) != "go" {
for tag, _ := range request.userAgent {
if tag == key {
request.userAgent[tag] = value
newkey = false
}
}
if newkey {
request.userAgent[key] = value
}
}
}
func (request *baseRequest) addHeaderParam(key, value string) {
request.Headers[key] = value
}
func (request *baseRequest) addQueryParam(key, value string) {
request.QueryParams[key] = value
}
func (request *baseRequest) addFormParam(key, value string) {
request.FormParams[key] = value
}
func (request *baseRequest) GetAcceptFormat() string {
return request.AcceptFormat
}
func (request *baseRequest) GetLocationServiceCode() string {
return request.locationServiceCode
}
func (request *baseRequest) GetLocationEndpointType() string {
return request.locationEndpointType
}
func (request *baseRequest) GetProduct() string {
return request.product
}
func (request *baseRequest) GetScheme() string {
return request.Scheme
}
func (request *baseRequest) SetScheme(scheme string) {
request.Scheme = scheme
}
func (request *baseRequest) GetMethod() string {
return request.Method
}
func (request *baseRequest) GetDomain() string {
return request.Domain
}
func (request *baseRequest) SetDomain(host string) {
request.Domain = host
}
func (request *baseRequest) GetPort() string {
return request.Port
}
func (request *baseRequest) GetRegionId() string {
return request.RegionId
}
func (request *baseRequest) GetHeaders() map[string]string {
return request.Headers
}
func (request *baseRequest) SetContentType(contentType string) {
request.addHeaderParam("Content-Type", contentType)
}
func (request *baseRequest) GetContentType() (contentType string, contains bool) {
contentType, contains = request.Headers["Content-Type"]
return
}
func (request *baseRequest) SetStringToSign(stringToSign string) {
request.stringToSign = stringToSign
}
func (request *baseRequest) GetStringToSign() string {
return request.stringToSign
}
type RpcRequest struct {
*baseRequest
}
type CommonRequest struct {
*baseRequest
Version string
ApiName string
Product string
ServiceCode string
EndpointType string
// roa params
PathPattern string
PathParams map[string]string
}
// SendSmsRequest is the request struct for api SendSms
type SendSmsRequest struct {
*requests.RpcRequest
ResourceOwnerId requests.Integer `position:"Query" name:"ResourceOwnerId"`
SmsUpExtendCode string `position:"Query" name:"SmsUpExtendCode"`
SignName string `position:"Query" name:"SignName"`
ResourceOwnerAccount string `position:"Query" name:"ResourceOwnerAccount"`
PhoneNumbers string `position:"Query" name:"PhoneNumbers"`
OwnerId requests.Integer `position:"Query" name:"OwnerId"`
OutId string `position:"Query" name:"OutId"`
TemplateCode string `position:"Query" name:"TemplateCode"`
TemplateParam string `position:"Query" name:"TemplateParam"`
}
#[derive(Default, Debug)]
pub struct BaseRequest {
pub Scheme: String,
pub Method: String,
pub Domain: String,
pub Port: String,
pub RegionId: String,
pub isInsecure: bool,
pub userAgent: HashMap<String, String>,
pub product: String,
pub version: String,
pub actionName: String,
pub AcceptFormat: String,
pub QueryParams: HashMap<String, String>,
pub Headers: HashMap<String, String>,
pub FormParams: HashMap<String, String>,
pub Content: Vec<u8>,
pub locationServiceCode: String,
pub locationEndpointType: String,
pub queries: String,
pub stringToSign: String,
}
pub trait BaseRequestExt {
fn base(&self) -> &BaseRequest;
fn base_as_mut(&mut self) -> &mut BaseRequest;
fn GetQueryParams(&self) -> &HashMap<String, String> {
self.base().QueryParams.borrow()
}
fn GetFormParams(&self) -> &HashMap<String, String> {
self.base().FormParams.borrow()
}
fn GetHTTPSInsecure(&self) -> bool {
self.base().isInsecure
}
fn SetHTTPSInsecure(&mut self, isInsecure: bool) {
self.base_as_mut().isInsecure = isInsecure
}
fn GetContent(&self) -> &[u8] {
self.base().Content.borrow()
}
fn SetContent(&mut self, content: &[u8]) {
self.base_as_mut().Content = content.to_owned()
}
fn SetVersion(&mut self, version: &str) {
self.base_as_mut().version = version.to_string();
}
fn GetVersion(&self) -> &str {
self.base().version.borrow()
}
fn GetActionName(&self) -> &str {
self.base().actionName.borrow()
}
fn GetUserAgent(&self) -> &HashMap<String, String> {
self.base().userAgent.borrow()
}
fn AppendUserAgent(&mut self, key: &str, value: &str) {
let mut newKey = true;
if self.base_as_mut().userAgent.is_empty() {
self.base_as_mut().userAgent = HashMap::new();
}
if strings::ToLower(key).as_str() != "core" && strings::ToLower(key) != "rust" {
for (tag, mut v) in self.base_as_mut().userAgent.iter_mut() {
if tag == key {
*v = value.to_string();
newKey = false;
}
}
if newKey {
self.base_as_mut()
.userAgent
.insert(key.to_string(), value.to_string());
}
}
}
fn addHeaderParam(&mut self, key: &str, value: &str) {
self.base_as_mut()
.Headers
.insert(key.to_string(), value.to_string());
}
fn addQueryParam(&mut self, key: &str, value: &str) {
self.base_as_mut()
.QueryParams
.insert(key.to_string(), value.to_string());
}
fn addFormParam(&mut self, key: &str, value: &str) {
self.base_as_mut()
.FormParams
.insert(key.to_string(), value.to_string());
}
fn GetAcceptFormat(&self) -> &str {
self.base().AcceptFormat.borrow()
}
fn GetLocationServiceCode(&self) -> &str {
self.base().locationServiceCode.borrow()
}
fn GetLocationEndpointType(&self) -> &str {
self.base().locationEndpointType.borrow()
}
fn GetProduct(&self) -> &str {
self.base().product.borrow()
}
fn SetProduct(&mut self, product: &str) {
self.base_as_mut().product = product.to_string();
}
fn GetScheme(&self) -> &str {
self.base().Scheme.borrow()
}
fn SetScheme(&mut self, scheme: &str) {
self.base_as_mut().Scheme = scheme.to_string()
}
fn GetMethod(&self) -> &str {
self.base().Method.borrow()
}
fn GetDomain(&self) -> &str {
self.base().Domain.borrow()
}
fn SetDomain(&mut self, host: &str) {
self.base_as_mut().Domain = host.to_string()
}
fn GetPort(&self) -> &str {
self.base().Port.borrow()
}
fn GetRegionId(&self) -> &str {
self.base().RegionId.borrow()
}
fn GetHeaders(&self) -> &HashMap<String, String> {
self.base().Headers.borrow()
}
fn SetContentType(&mut self, contentType: &str) {
self.addHeaderParam("Content-Type", contentType)
}
fn GetContentType(&self) -> Option<&str> {
self.base().Headers.get("Content-Type").map(|s| s.as_str())
}
fn SetStringToSign(&mut self, stringToSign: &str) {
self.base_as_mut().stringToSign = stringToSign.to_string()
}
fn GetStringToSign(&self) -> &str {
self.base().stringToSign.borrow()
}
}
#[derive(Default, Debug)]
pub struct RpcRequest {
base: BaseRequest,
}
impl BaseRequestExt for RpcRequest {
fn base(&self) -> &BaseRequest {
self.base.borrow()
}
fn base_as_mut(&mut self) -> &mut BaseRequest {
self.base.borrow_mut()
}
}
pub struct CommonRequest {
base: BaseRequest,
pub Version: String,
pub ApiName: String,
pub Product: String,
pub ServiceCode: String,
pub EndpointType: String,
// roa params
pub PathPattern: String,
pub PathParams: HashMap<String, String>,
pub Ontology: AcsRequest,
}
impl BaseRequestExt for CommonRequest {
fn base(&self) -> &BaseRequest {
self.base.borrow()
}
fn base_as_mut(&mut self) -> &mut BaseRequest {
self.base.borrow_mut()
}
}
#[derive(Default, Debug)]
pub struct SendSmsRequest {
pub rpcRequest: requests::RpcRequest,
pub ResourceOwnerId: requests::Integer, //`position:"Query" name:"ResourceOwnerId"`
pub SmsUpExtendCode: String, //`position:"Query" name:"SmsUpExtendCode"`
pub SignName: String, //`position:"Query" name:"SignName"`
pub ResourceOwnerAccount: String, //`position:"Query" name:"ResourceOwnerAccount"`
pub PhoneNumbers: String, //`position:"Query" name:"PhoneNumbers"`
pub OwnerId: requests::Integer, //`position:"Query" name:"OwnerId"`
pub OutId: String, //`position:"Query" name:"OutId"`
pub TemplateCode: String, //`position:"Query" name:"TemplateCode"`
pub TemplateParam: String, //`position:"Query" name:"TemplateParam"`
}
impl BaseRequestExt for SendSmsRequest {
fn base(&self) -> &BaseRequest {
self.rpcRequest.base()
}
fn base_as_mut(&mut self) -> &mut BaseRequest {
self.rpcRequest.base_as_mut()
}
}
impl SendSmsRequest {
pub fn BuildQueryParams(&mut self) {
self.addQueryParam("SignName", &self.SignName.to_owned());
self.addQueryParam("PhoneNumbers", &self.PhoneNumbers.to_owned());
self.addQueryParam("TemplateCode", &self.TemplateCode.to_owned());
self.addQueryParam("ResourceOwnerId", &self.ResourceOwnerId.to_owned());
self.addQueryParam("SmsUpExtendCode", &self.SmsUpExtendCode.to_owned());
self.addQueryParam(
"ResourceOwnerAccount",
&self.ResourceOwnerAccount.to_owned(),
);
self.addQueryParam("OwnerId", &self.OwnerId.to_owned());
self.addQueryParam("OutId", &self.OutId.to_owned());
self.addQueryParam("TemplateParam", &self.TemplateParam.to_owned());
}
} |
@Iron-E regarding
although this isn't solved automatically, the |
@wandercn The question is, why do we need to use |
@SOF3
|
@wandercn You can use this crate https://crates.io/crates/delegate |
@wandercn That's what I'm saying, why do we want composite structures? How is Even in OOP languages like Java, it is typically considered an antipattern to have deep hierarchies of inheritance. Composite structures do not save us from all the issues of inheritance. Having Instead of type Foo struct { Base, Other Fields }
type Bar struct { Foo, Other Fields } Why not type Foo struct { Other Fields }
type Bar struct { Base, Foo, Other Fields } ? At least in that case it is more explicit that struct Request {
uid: u128,
}
struct NewHorseRequest {
base: Request,
name: String,
}
struct NewWhiteHorseRequest {
base: HorseRequest,
whiteness: f32,
}
fn handle_horse(horse: &HorseRequest) {
colorizer.set_color(&horse.name, random_color());
}
To conclude, having a type contain a type that contains another type (i.e. more than one level of composition) while the outermost type has direct relationship to the innermost type is an antipattern, no matter we have code reuse (through inheritance or struct composition) or not. This antipattern just becomes typically less obvious in Java, Go, etc. because people are too used to the ambiguous information represented by type information, but becomes more apparent in Rust because people are writing code "correctly" (my subjective opinion) such that self-explanatory function signatures become possible. |
Using this crate is still the best way I found to reuse code, at present. |
Motivation
Data structures which closely fit a single inheritance model can be very efficiently implemented in C++. Where high performance (both space and time) is crucial there is distinct disadvantage in using Rust for programs which widely use such data structures. A pressing example is the DOM in Servo. For a small example in C++, see https://gist.github.com/jdm/9900569. We require some solution which satisfies the following requirements:
fn foo(JSRef<T>, ...)
;Status
There has been discussion of potential solutions on discuss (http://discuss.rust-lang.org/t/summary-of-efficient-inheritance-rfcs/494) and in several meetings (minutes and minutes).
We clarified the requirements listed above (see the minutes for details) and established that an ergonomic solution is required. That is, we explicitly don't want to discourage programmers from using this feature by having an unfriendly syntax. We also summarised and evaluated the various proposals (again, see the minutes for details). We feel that no proposal 'as is' is totally satisfactory and that there is a bunch of work to do to get a good solution. We established a timeline (see below) for design and implementation. We would like to reserve a few keywords to reduce the backwards compatibility hazard (#342).
Plan
In December the Rust and Servo teams will all be in one place and we intend to make decisions on how to provide an efficient code reuse solution and plan the implementation in detail. We'll take into account the discussions on the various RFC and discuss comment threads and of course all the community members who attend the Rust weekly meetings will be invited. We will take and publish minutes. This will lead to a new RFC. We expect implementation work to start post-1.0. If we identify backwards compatibility hazards, then we'll aim to address these before the 1.0 RC.
RFC PRs
There have been numerous RFC PRs for different solutions to this problem. All of these have had useful and interesting parts and earlier RFCs have been heavily cannibalised by later ones. We believe that RFC PRs #245 and #250 are the most relevant and the eventual solution will come from these PRs and/or any ideas that emerge in the future. For a summary of some of the proposals and some discussion, see this discuss thread.
The text was updated successfully, but these errors were encountered: