3 releases
0.0.1-alpha.2 | Oct 18, 2019 |
---|---|
0.0.1-alpha | Sep 15, 2019 |
0.0.0 | Aug 9, 2019 |
#2418 in Rust patterns
14KB
52 lines
::inheritance
This (experimental) crate provides procedural macros to get "inheritance-like" behavior by deriving delegation from composition.
Presentation
Imagine having some Point
type and some behavior / associated methods:
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: f32,
y: f32,
}
impl Point {
fn x (self: &'_ Self) -> f32
{
self.x
}
fn y (self: &'_ Self) -> f32
{
self.y
}
fn name (self: &'_ Self) -> Option<&'_ str>
{
Some("Point")
}
}
Now imagine you want to have a new "Point
-like" type, but with extra
attributes (and maybe some overriden behavior):
# struct Point;
struct NamedPoint {
name: String,
point: Point,
}
First of all, being "Point
-like" requires abstracting the inherent methods
under a trait:
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: f32,
y: f32,
}
trait IsPoint {
fn x (self: &'_ Self) -> f32;
fn y (self: &'_ Self) -> f32;
fn name (self: &'_ Self) -> Option<&'_ str>;
}
impl IsPoint for Point {
fn x (self: &'_ Self) -> f32
{
self.x
}
fn y (self: &'_ Self) -> f32
{
self.y
}
fn name (self: &'_ Self) -> Option<&'_ str>
{
Some("Point")
}
}
Now we'd like NamedPoint
to implement IsPoint
:
# #[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}
struct NamedPoint {
name: String,
point: Point,
}
impl IsPoint for NamedPoint {
#[inline]
fn x (self: &'_ Self) -> f32
{
self.point.x()
}
#[inline]
fn y (self: &'_ Self) -> f32
{
self.point.y()
}
#[inline]
fn name (self: &'_ Self) -> Option<&'_ str>
{
self.point.name()
}
}
This leads to writing very repetitive and uninteresting (delegation) code...
Enter ::inheritance
This last implementation can be completely skipped by slapping a
#[inheritable]
attribute on the IsPoint
trait, and a Inheritance
derive
on the NamedPoint
struct:
use ::inheritance::{inheritable, Inheritance};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: f32,
y: f32,
}
#[inheritable]
trait IsPoint {
fn x (self: &'_ Self) -> f32;
fn y (self: &'_ Self) -> f32;
fn name (self: &'_ Self) -> Option<&'_ str>;
}
impl IsPoint for Point {
fn x (self: &'_ Self) -> f32
{
self.x
}
fn y (self: &'_ Self) -> f32
{
self.y
}
fn name (self: &'_ Self) -> Option<&'_ str>
{
Some("Point")
}
}
#[derive(Inheritance)]
struct NamedPoint {
name: String,
#[inherits(IsPoint)]
point: Point,
}
nightly
Rust
This feature is especially interesting with the specialization
feature, which
you can already use in nightly.
When depending on the inheritance
crate in your Cargo.toml
, you can specify
that you want to use this feature:
[dependencies]
inheritance = { version = "...", features = ["specialization"] }
You will then be able to override some of the auto-generated delegation methods:
# use::inheritance::{inheritable, Inheritance};#[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}#[inheritable]trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}
#[derive(Inheritance)]
struct NamedPoint {
name: String,
#[inherits(IsPoint)]
point: Point,
}
# #[cfg(feature = "specialization")]
impl IsPoint for NamedPoint {
fn name (self: &'_ Self) -> Option<&'_ str>
{
Some(&*self.name)
}
}
Going further
Newtypes
The #[inherits(...)]
field attribute can be used on tuple structs fields,
which makes it the perfect tool for newtypes:
# use::inheritance::{inheritable, Inheritance};#[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}#[inheritable]trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}
#[derive(Inheritance)]
struct AnonymousPoint (
#[inherits(IsPoint)]
Point,
);
# #[cfg(feature = "specialization")]
impl IsPoint for AnonymousPoint {
fn name (self: &'_ Self) -> Option<&'_ str>
{
None
}
}
Multiple "inheritance"
A single struct with a #[derive(Inheritance)]
on it can have multiple
#[inherits(...)]
on either the same field or multiple distinct fields, since
the implementation of each "inherited" trait will just be delegating the
methods of that trait onto the decorated field. If you try to "inherit" the same
trait from multiple fields, then classic coherence rules will prevent it:
# use::inheritance::{inheritable, Inheritance};#[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}#[inheritable]trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Color {
Red,
Green,
Blue,
}
#[inheritable]
trait Colored {
fn color (self: &'_ Self) -> Color;
}
impl Colored for Color {
#[inline]
fn color (self: &'_ Self) -> Color
{
*self
}
}
#[derive(Inheritance)]
struct ColoredPoint {
#[inherits(IsPoint)]
point: Point,
#[inherits(Colored)]
pigments: Color,
}
Virtual dispatch
The idea behind "virtual" methods, à la C++, is that objects always always
carry a vtable with some methods in it, the ones marked virtual
(and with
the other methods never in it). In Rust, however, "objects" sometimes carry
a vtable (i.e., struct
s and enum
s do not, but trait objects (dyn Trait
)
do), and this vtable contains all the methods of the trait and its
supertraits:
Imagine having a .present()
method whose body calls .name()
, and where the
behavior / value returned by the .name()
call changes for each different
Point
.
/// somewhere in the code
fn present (self: &'_ Self) -> Cow<'static, str>
// where Self : IsPoint
{
if let Some(name) = self.name() {
Cow::from(format!("{}({}, {})", name, self.x(), self.y()))
} else {
Cow::from("<anonymous>")
}
}
let point = Point { x: 42., y: 27. };
assert_eq!(
&point.present() as &str,
"Point(42.0, 27.0)"
);
let anonymous_point = AnonymousPoint(point);
assert_eq!(
&anonymous_point.present() as &str,
"<anonymous>"
);
let named_point = NamedPoint { name: "Carré", point };
assert_eq!(
&named_point.present() as &str,
"Carré(42.0, 27.0)"
);
-
In a language such as C++, this can be achieved by tagging the
.name()
method asvirtual
, and by then having.present()
be a method of the base class (not necessarilyvirtual
itself). -
In Rust the above strategy cannot be done; one must use a function / method polymorphic over implementors of the
IsPoint
type, using:-
dynamic dispatch (the mechanism in C++ behind
virtual
methods):# use::inheritance::{inheritable, Inheritance};#[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}#[inheritable]trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}use::std::borrow::Cow; impl dyn IsPoint + '_ { fn present (self: &'_ Self) -> Cow<'static, str> { if let Some(name) = self.name() { Cow::from(format!("{}({}, {})", name, self.x(), self.y())) } else { Cow::from("<anonymous>") } } }
-
static dispatch; then getting method-like syntax requires a helper trait:
# use::inheritance::{inheritable, Inheritance};#[derive(Debug,Clone,Copy,PartialEq)]struct Point{x:f32, y:f32}#[inheritable]trait IsPoint{fn x(&self)->f32;fn y(&self)->f32;fn name(&self)->Option<&str>;}impl IsPoint for Point{fn x(&self)->f32{self.x}fn y(&self)->f32{self.y}fn name(&self)->Option<&str>{Some("Point")}}use::std::borrow::Cow; trait Present where Self : IsPoint, { fn present (self: &'_ Self) -> Cow<'static, str> { if let Some(name) = self.name() { Cow::from(format!("{}({}, {})", name, self.x(), self.y())) } else { Cow::from("<anonymous>") } } } impl<T : ?Sized> Present for T where T : IsPoint, {}
-
Currently the macro does not target full-featured "virtual" methods, so
overriding .present()
itself is not yet possible.
Since this is an experimental crate, I will try to explore that possibility...
Dependencies
~2MB
~48K SLoC