Open nrc opened 9 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.
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!
@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.
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).
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-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`
@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.
我认为写一个完整一点的例子比较起来比较直观, @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()
}
}
详细的对照代码如下:
// 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"`
}
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()
}
}
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()
}
}
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
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.
@wandercn The question is, why do we need to use SendSmsRequest
itself as a BaseRequest
? Why can't users just call request.base()
directly?
@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();
@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 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.
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.
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.