Part of the “Rustober” series.

As I said in the first post of the series, parts of Rust are remarkably similar to Swift, such as Option<T> and Result<T>. Let’s try to compare Rust traits to Swift protocols. I’m very new to Rust and I’m not aiming for completeness, so take it with a grain of salt.

Looking at them both, Swift leans towards developer ergonomics (many things are implicit, less strict rules around what can be defined where) and Rust leans towards compile-time guarantees: there’s less flexibility but also less ambiguity.

For example, in Swift you can add multiple protocol conformances at once, and the compiler will pick up any types that are named the same as associated types:

protocol Describable {
    func describe() -> String
}

protocol Greetable {
    associatedtype Name

    func greet() -> String
    func farewell() -> String
}

// Default implementation can be added later
extension Greetable {
    func greet() -> String { "Hello!" }
    func farewell() -> String { "Bye!" }
}

struct Person: Describable {
    enum Name {}

    // Required method implemented at time of conformance
    func describe() -> String { "Person" }
}

// Associated type automatically picked up
extension Person: Greetable {}

// Type-specific implementation can be added later
extension Person {
    func farewell() -> String { "See ya!" }
}

let person = Person()
print(person.farewell()) // Prints "See ya!"

And in Rust:

trait Describable {
    // Explicit addition of "&self" to indicate instance method
    fn describe(&self) -> String;
}

trait Greetable {
    type Name; // Associated type

    // Default implementation needs to be done as part of
    // trait definition
    fn greet(&self) -> String {
        "Hello!".to_string()
    }

    // Cannot add default implementation later
    fn farewell(&self) -> String {
        "Bye!".to_string()
    }
}

struct Person;
// Can't nest definitions directly
struct Name;

// Separate impl blocks are needed for each trait
impl Describable for Person {
    fn describe(&self) -> String {
        "Person".to_string()
    }
}

impl Greetable for Person {
    // Need to explicitly define the associated type
    type Name = Name;

    // Can't override the default implementation later
    fn farewell(&self) -> String {
        "See ya!".to_string()
    }
}

fn main() {
    let person = Person;
    println!("{}", person.farewell());  // Prints "See ya!"
}

Even this short example shows how flexible Swift is — and we haven’t even seen generics yet. I’m convinced Rust generics in traits are better done than in Swift partly because they are more granular. Whenever I tried to compose anything complicated out of Swift protocols, I always ran into problems either with “Self or associated type requirements” (when a protocol can only be used a generic constraint) or existential types.

Here’s a real example where Swift couldn’t help me constrain an associated type on a protocol, so I had to leave it simply as an associated type without additional conformance. The idea is to have a service that would be able to swap between multiple instances of concrete providers, all conforming to several different types and ultimately descending (in the protocol sense, not class sense) from one common ancestor.

protocol CommonAncestor {
    var isFish: Bool { get }
}

protocol LessCommonAncestor: CommonAncestor {}

protocol DNAService {
    // Doesn't work, need to remove CommonAncestor constraint
    associatedtype Ancestor: CommonAncestor

    var dnaProvider: Ancestor { get }
}

// Concrete provider
class Chap: LessCommonAncestor {
    let isFish = false
}

// 1
// Error: No type for 'Self.Ancestor' can satisfy both
// 'Self.Ancestor == any LessCommonAncestor' and 'Self.Ancestor : CommonAncestor'
// extension DNAService where Ancestor == LessCommonAncestor {}

// 2
// With ":" instead of "==" we get errors 3 and 4 below
extension DNAService where Ancestor: LessCommonAncestor {}

// Concrete service
class HumanService: DNAService {
    // 3
    // Error: Cannot infer 'Ancestor' = 'any LessCommonAncestor'
    // because 'LessCommonAncestor' as a type cannot conform to protocols
    let dnaProvider: LessCommonAncestor

    // 4
    // Error: property declares an opaque return type, but has no
    // initializer expression from which to infer an underlying type
    let dnaProvider: some LessCommonAncestor

    init(dna: LessCommonAncestor = Chap()) {
        self.dnaProvider = dna
    }
}

// 5
// Before you ask why I didn't do this :D
class HumanService<T: LessCommonAncestor>: DNAService {
    …
    // Error: Cannot assign value of type
    // 'any LessCommonAncestor' to type 'T'
    init(dna: LessCommonAncestor = Chap()) {
        self.dnaProvider = dna
    }
}

Here’s similar code in Rust which does not have this problem:

trait CommonAncestor {
    // Rust traits can't define properties, only methods
    fn is_fish(&self) -> bool;
}

trait LessCommonAncestor: CommonAncestor {}

trait DNAService {
    type Ancestor: CommonAncestor;

    fn dna_provider(&self) -> &Self::Ancestor;
}

// Concrete provider
struct Chap;

impl CommonAncestor for Chap {
    fn is_fish(&self) -> bool { false }
}

impl LessCommonAncestor for Chap {}

// Concrete service
struct HumanService<T: LessCommonAncestor> {
    dna_provider: T,
}

impl<T: LessCommonAncestor> DNAService for HumanService<T> {
    type Ancestor = T;

    fn dna_provider(&self) -> &Self::Ancestor {
        &self.dna_provider
    }
}

fn main() {
    // Can swap between concrete providers at compile time
    let service = HumanService { dna_provider: Chap };
    // Prints "Is fish? false"
    println!("Is fish? {}", service.dna_provider().is_fish());
}

I’m looking forward to exploring the differences (and similarities) (and bashing my head on the wall) when I get to write some actual Rust code.