rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.87k stars 1.56k forks source link

Efficient code reuse #349

Open nrc opened 9 years ago

nrc commented 9 years ago

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:

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.

SOF3 commented 2 years ago

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.

Iron-E commented 2 years ago

This is a nice none-hacky approach but how do you solve wanting to match on a base type and do different things depending on the sub type?

let base = get_some_subtype();

match base {
  SubType1 => ...
  SubType2 => ...
}

@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. &dyn Foo) in Rust— only &dyn Any and &dyn Error (afaik). Hopefully that changes in the future!

mrahhal commented 2 years ago

@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.

SOF3 commented 2 years ago

provides the ability to pattern match on type kinds

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).

mrahhal commented 2 years ago

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.

fogti commented 2 years ago

@mrahhal a partial solution for this would be combining enum-derive (or something like that) to expose traits defined for contained types, and introducing a way to flatten enums (so, like kind of inheritance for enums, the enums which "get" (?) inherited from must be exhaustive; similar to the "mixin" concept in other languages), like:

enum UnaryExpr {
    A, B,
}
enum BinaryExpr {
    X, Y,
}
#[inherit(UnaryExpr, BinaryExpr)]
enum Expr {}
// `Expr` contains variants `UnaryExpr`, `BinaryExpr`
mrahhal commented 2 years ago

@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 Expr contains a UnaryExpr variant, UnaryExpr itself is not an Expr. This means I can't share common functionality (again, without resorting to messy macros and a lot of boilerplate). I also wouldn't say this is equivalent to mixins. You're just nesting types inside other types here, whereas mixins are pretty much composition of logic inside the same type.

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 eval("{code as a string}") everywhere). I tried every kind of workaround you can think of, none can replace a proper code reuse feature.

wandercn commented 2 years ago

我认为写一个完整一点的例子比较起来比较直观, @ibraheemdev @Iron-E @mrahhal @zseri

以下是golang代码通过结构体内嵌实现的属性和方法复用代码方式,这是golang面向对象的处理方式,确实很方便。怎么转换到 rust的 trait方式复用代码呢? rust中实现的时候把golang中的baseRequest结构体的方法,抽象到trait BaseRequestExt中,trait BaseRequestExt中的 fn base(&self) 和 fn base_as_mut(&mut self) 方法,需要在每次复用的时候重写。其他的代码直接复用trait BaseRequestExt中的方法,我觉得还是非常高效的。

impl BaseRequestExt for SendSmsRequest {
    fn base(&self) -> &BaseRequest {
        self.rpcRequest.base()
    }

    fn base_as_mut(&mut self) -> &mut BaseRequest {
        self.rpcRequest.base_as_mut()
    }
}

详细的对照代码如下:

  1. go实现面向对象复用代码的例子

// 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"`
}
  1. 对应rust中通过trait 实现面向对象复用代码的对照代码例子。

[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()); } }

fogti commented 2 years ago

@Iron-E regarding

you can't downcast any-old trait object (e.g. &dyn Foo) in Rust— only &dyn Any and &dyn Error (afaik). Hopefully that changes in the future!

although this isn't solved automatically, the downcast-rs, as-any (the second crate, written by myself, is a bit simpler, and less powerful) crates partially solve this.

SOF3 commented 2 years ago

@wandercn The question is, why do we need to use SendSmsRequest itself as a BaseRequest? Why can't users just call request.base() directly?

wandercn commented 2 years ago

@SOF3 就是为了实现跟golang一样的内嵌结构体,直接组合复用代码,组合看成一个整体。

pub struct SendSmsRequest {
         pub base BaseRequest
         pub comm CommRequest
         pub other OtherRequest
}

let request =SendSmsRequest::default();
调用组合的方法可以类似如下这样,把组合进来的方法都看成是SendSmsRequest的方法,使用的人不需要知道方法内部具体来源直接看成一个整体。:
request.baseFunc() ;
request.commFunc();
request.otherFunc();

避免下面的方式:
request.base().Func() ;
request.comm().Func();
request.other().Func();
如果每个组合的base ,comm,other也有组合别的结构体。
调用内部方法可能变成:
request.base().base().a()...Func() ;
request.base().base().b()...Func() ;
request.comm().comm().c().....Func();
request.comm().comm().d().....Func();
request.other().other().e().......Func();
request.other().other().f().......Func();
12101111 commented 2 years ago

@wandercn You can use this crate https://crates.io/crates/delegate

SOF3 commented 2 years ago

@wandercn That's what I'm saying, why do we want composite structures? How is request.base_func() any better than request.base.func()?

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 base nested deeply inside is probably not that sensible anyway.

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 Foo itself should be used as a component instead of a request itself, because a Bar request is indeed not a Foo request, and it would be confusing for a Bar request to contain a Foo request. I am not sure whether self-descriptive type system is a popular concept in the Go language, but for Rust, many APIs are already clear enough what they intend to do just by looking at the type signature, so if you have a function that accepts a HorseRequest, you probably expect that an actual, self-contained HorseRequest was sent, and if someone actually sent a WhiteHorseRequest that contains a HorseRequest, you would end up with a condition where 白馬非馬. One confusing example is like this:

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());
}

handle_horse expects to assign a color to a horse request. Originally it couldn't go wrong. But since a NewWhiteHorseRequest also contains a NewHorseRequest, we end up allowing people to misuse the handle_horse API for white horse requests, which should originally be impossible.

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.

dbsxdbsx commented 1 year ago

There is discussion of delegation in the language in #2393 and discussions of delgation proc macros like delegate linked from https://www.reddit.com/r/rust/comments/ccqucx/delegation_macro/etpfud5/?utm_source=reddit&utm_medium=web2x&context=3

Using this crate is still the best way I found to reuse code, at present.