Thursday, April 21, 2022

Go vs Rust - Interface vs Trait

 Interfaces in Go

An interface type is defined as a set of method signatures. A value of interface type can hold any value that implements those methods that is "Duck Typing". Implicit interfaces decouple the definition of an interface from its implementation.

package main
import (
    "fmt"
)
type Phone interface {
    call()
}
type IPhone struct {
}
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}
func main() {
    var phone Phone
    phone = new(IPhone)
    phone.call()
}

Interface Inheritance

type Eater interface {
Eat()
}

type Runner interface {
Run()
}

type Animal interface {
Eater
Runner
}

Empty Interface

The empty interface type essentially describes no methods. It has no rules. And because of that, it follows that any and every object satisfies the empty interface.

package main

import "fmt"

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
var i interface{}
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

Type Assertions

A type assertion provides access to an interface value's underlying concrete value. e.g. t := i.(T) or t, ok := i.(T)

if t, ok := i.(*S); ok {
    fmt.Println("s implements I", t)
}
or
switch t := i.(type) {
case *S:
    fmt.Println("i store *S", t)
case *R:
    fmt.Println("i store *R", t)
}

Trait in Rust

Traits are an abstract definition of shared behavior amongst different types. So, we can say that traits are to Rust what interfaces are to Java or abstract classes are to C++. A trait method is able to access other methods within that trait.

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

let tweet = Tweet {
        username: String::from("horse_ebooks"),
};

println!("1 new tweet: {}", tweet.summarize());

Traits as Parameters

The impl Trait syntax is convenient and makes for more concise code in simple cases. The trait bound syntax can express more complexity in other cases.

If we wanted this function to allow item1 and item2 to have different types, using impl Trait would be appropriate (as long as both types implement Summary).

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

If we wanted to force both parameters to have the same type, that’s only possible to express using a trait bound, like this:

pub fn notify<T: Summary>(item1: &T, item2: &T) {}

Trait Combos

Specifying Multiple Trait Bounds with the + Syntax

pub fn notify(item: &(impl Summary + Display)) {}
or
pub fn notify<T: Summary + Display>(item: &T) {}

Clearer Trait Bounds with where Clauses

instead of writing this:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

we can use a where clause, like this:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

Supertraits

Rust has a way to specify that a trait is an extension of another trait, giving us something similar to subclassing in other languages.

trait Shape { fn area(&self) -> f64; }
trait Shape2 { fn area(&self) -> f64; }
trait Circle : Shape+Shape2 { fn area(&self) -> f64; }

Trait Constants

Traits can also have associated constants. This is less common than trait methods, but is not without its uses. Like with methods, constants may provide a default value.

trait ConstTrait {
    const GREETING: &'static str;
    const NUMBER: i32 = 42;
}

Using Trait Bounds to Conditionally Implement Methods

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Associated Types

Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used in this type’s place for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        // --snip--

This syntax seems comparable to that of generics. The difference is that when using generics, we must annotate the types in each implementation; because we can also implement Iterator<String> for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. 

With associated types, we don’t need to annotate types because we can’t implement a trait on a type multiple times.

Default Generic Type Parameters and Operator Overloading

When we use generic type parameters, we can specify a default concrete type for the generic type. This eliminates the need for implementors of the trait to specify a concrete type if the default type works.

struct Point {
    x: i32,
    y: i32,
}
impl Add for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}
fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

The default generic type in this code is within the Add trait. Here is its definition

trait Add<Rhs=Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

This code should look generally familiar: a trait with one method and an associated type. The new part is Rhs=Self: this syntax is called default type parameters. 

When we implemented Add for Point, we used the default for Rhs because we wanted to add two Point instances. Let’s look at an example of implementing the Add trait where we want to customize the Rhs type rather than using the default.

use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
    type Output = Millimeters;
    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name

Instance methods

trait Pilot {
    fn fly(&self);
}
trait Wizard {
    fn fly(&self);
}
struct Human;
impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}
impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}
impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}
fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Static Methods

trait Animal {
    fn baby_name() -> String;
}
struct Dog;
impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}
impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}
fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

Using the Newtype Pattern to Implement External Traits on External Types

 the orphan rule that states we’re allowed to implement a trait on a type as long as either the trait or the type are local to our crate. It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct.

Newtype is a term that originates from the Haskell programming language. There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.

As an example, let’s say we want to implement Display on Vec<T>, which the orphan rule prevents us from doing directly because the Display trait and the Vec<T> type are defined outside our crate. We can make a Wrapper struct that holds an instance of Vec<T>; then we can implement Display on Wrapper and use the Vec<T> value.

use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}
fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

The downside of using this technique is that Wrapper is a new type, so it doesn’t have the methods of the value it’s holding. We would have to implement all the methods of Vec<T> directly on Wrapper such that the methods delegate to self.0, which would allow us to treat Wrapper exactly like a Vec<T>. If we wanted the new type to have every method the inner type has, implementing the Deref trait on the Wrapper to return the inner type would be a solution.








No comments:

Post a Comment