Walking through "The Java Tutorials" with Rust
Reading 'What Is an Interface?' and trying Rust's impl specialization
A terribly hot evening in July 2021As we mentioned in the previous post, Java interfaces have some advantages over class inheritance - to name one, a Java class can implement any number of interfaces. This lesson in the java tutorials is describing what is an interface and demonstrates defining and implementing a Bicycle
interface.
Lesson link: What Is an Interface?
Java's Interfaces and Rust's Traits
Rust traits and java interfaces are conceptually close:
- Both define the behavior a struct/class should implement.
- There are compile type checks that call sites that expect specific trait/interface would allow only these objects.
Rust traits - a short recap
We showed an example of a Bicycle trait and Basic/MountainBicycle structs that implement it in our first attempt to mimic inheritance with traits:
- The code there was quite similar to the java example the current lesson
trait Bicycle { // java: interface Bicycle
...
impl Bicycle for BasicBicycle { // java: class BasicBicycle implements Bicycle
...
impl Bicycle for MountainBicycle { // java: class MountainBicycle implements Bicycle
...
- In Rust the concrete type must be known in compile time unless one uses the
dyn
keyword.
Coding for an interface
Suppose we don't need runtime polymorphism or to force a superclass-subclass model, Rust provides other mechanisms that allow "coding for an interface" i.e writing generic code that can operate on different concrete types that implement specific trait/s.
For example, suppose we define a take_a_ride()
method on Bicycles and would like to have a default implementation for all Bicycle objects. I can think of two rust statements I can use:
- Generic impl block (which I thought was a great idea, and felt natural to write)
- Trait default implementation.
Generic impl block bounded by the Bicycle
trait
Define a Ridable trait, and provide a generic implementation for every object that implements the Bicycle trait
trait Ridable {
fn take_a_ride(&mut self);
}
impl<T> Ridable for T where T : Bicycle {
fn take_a_ride(&mut self){
&self.speed_up(10);
}
}
// Caveat: this will collide with other impl Ridable that happen to match Bicycle
// ...
#[test]
fn take_a_ride_test(){
let mut bike = BasicBicycle::default();
bike.take_a_ride();
assert!(bike.get_speed() == 10);
}
Note that in current stable rust (version 1.5.3), looks like rust doesn't allow to define multiple impl blocks that can match a specific instance. For example, suppose we define a Special trait, mark MountainBicycle as special, and would like to override the above Ridable impl block just for Special Bicycles:
trait Special : Bicycle {}
impl Special for MountainBicycle {}
// This impl block won't compile
impl<T> Ridable for T where T : Special{
fn take_a_ride(&mut self){
&self.speed_up(3);
}
}
// Compiler would complain about "conflicting implementation"
35 | impl<T : Bicycle> Ridable for T{
| ------------------------------- first implementation here
...
41 | impl<T> Ridable for T where T : Special{
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation
After some searching, I've found that this was proposed a few years ago (see RFC: impl specialization, and Tracking issue for specialization) and called "specialization". There is already a feature flag that I can use to partially enable it in rust nightly called min_specialization. It enables overriding a generic impl block for specific types (but not traits), so using min_specialization and rust nightly we can write:
#![feature(min_specialization)]
impl<T : Bicycle> Ridable for T{
default fn take_a_ride(&mut self){
&self.speed_up(10);
}
}
impl Ridable for MountainBicycle {
fn take_a_ride(&mut self){
&self.speed_up(3);
}
}
BTW - changing to rust nightly is quite simple
# install nightly toolchain
$ rustup toolchain install nightly
# set rustup to use nightly in a specific directory
$ cd to_the_folder_where_I_want_nighly
$ rustup override set nightly
Note to self - learn about rust/functional alternatives to Java's approach
Both inheritance and specialization (at least, overriding implementations in some hierarchy of interfaces) are models that I used to have in Java. I found that even though they are extensively discussed in various rust-lang RFCs, there are still design gaps and tradeoffs to consider before they could be available in rust. Some of the gaps stem from rust design goals that Java doesn't share, but probably because there are alternatives in many situations (which I don't know about yet, so please drop a hint if you reached here. The rust book gives a taste of this in the chapter Implementing an Object-Oriented Design Pattern, and I suppose I can learn things from Scala programs that share some functional characteristics with Rust).
Trait default implementation
Rust provides a notion of Subtraits which adds more knowledge about the implementing type (I recommended reading the tour of Rust's standard library traits, see for example the section about Subtraits and Supertraits). Using it we can define a new trait that's available for types that implement the Bicycle trait. Like any other trait, we can provide a default implementation for types that are marked as implementing the Ridable trait and "override" it for specific types.
trait Ridable : Bicycle { // Ridable is a new trait available for types that implement Bicycle
fn take_a_ride(&mut self) { // default implementation
&self.speed_up(10);
}
}
impl Ridable for BasicBicycle {}
// should explicitly opt in to make BasicBicycle 'Ridable' and have the default implementation.
// Also, I wish I could end this line with `;` and not an empty block `{}` :-)
// provide different impl for MountainBicycle
impl Ridable for MountainBicycle {
fn take_a_ride(&mut self){
&self.speed_up(3);
}
}
What I'm missing?
Trying to compile the example in this post surprised me a bit, and I'm left with the feeling that I'm missing something important. I'll edit this post later with links to new findings. Moving on for now.