Does Rust support Duck typing?

The chapter 17 of TRPL book talks about static/dyn dispatch using trait and duck typing. From what I understand duck typing does not need any nominal types.

Wikipedia for duck typing has the following example for duck typing.

class Duck:
    def swim(self):
        print("Duck swimming")

    def fly(self):
        print("Duck flying")

class Whale:
    def swim(self):
        print("Whale swimming")

for animal in (Duck(), Whale()):
    animal.swim()
    animal.fly()

But in case of trait the rust compiler needs the concrete types to use a specific Trait type (by name) to do either static or dynamic binding. Given that how do we say Rust supports duck typing?

What did I miss?
Thanks sowmy

Note that the book does not say that Rust supports duck typing; instead, it says that what Rust does is similar in some ways to duck typing. It also discusses ways that it is different from duck typing.

3 Likes

Fair enough. The comparison to duck typing in a lang with nominal type system threw me off.

Thanks for your response.

1 Like

Also note that from a software engineering perspective, ducktyping is strictly inferior to static typing (and especially so w.r.t. static typing with type inference), both because errors are reported at runtime rather than compile time (which Python of course doesn't have), and because anything that looks like some specific interface will automatically be seen as implementing that interface, even if the author doesn't have any intention of supporting that interface.

5 Likes

Duck typing is not related to nominal vs structural typing. Also do note that Rust's type system is not purely nominal.

1 Like

Depending on the vision of duck typing, one could argue that any form of extensible polymorphism, which Rust features through traits (be it dyn traits or generics) are already a start for duck typing, albeit a well-typed one.


For full non-type-checked-as-a-whole duck typing, which I think is the example most people have in mind, and is definitely what that Python example does, in Rust, this can be featured:

  • Through macros

    (much like duck typing has happened for eons in C++ through templates (one of the things which made SFINAE thrive, I'd say))

    struct Duck();
    impl Duck {
        fn swim (self: &'_ Duck)
        {
            println!("Duck swimming");
        }
    }
    
    struct Whale();
    impl Whale {
        fn swim (self: &'_ Whale)
        {
            println!("Whale swimming");
        }
    }
    
    fn main ()
    {
        duck_typed_for!(animal in [Duck(), Whale()] {
            animal.swim();
        });
    }
    
  • Through distinct prefix-paths-but-same-suffix-API, and/or naming conventions

    • Most of the primitive integers types feature a fixed API which is common to all of them (.to_ne_bytes(), etc.), to allow easily swapping between one another, which is indeed especially useful for macros :slight_smile:

    • Rc and Arc share almost all of their APIs precisely for this reason: this way, again, they can be easily swapped with one another; e.g., through cfgs or just manual refactorings.

      In a similar fashion, for third-party crates out there, ::once_cell features both a sync and unsync modules, with, again, near identical APIs.

      Last but not least, the unmaintained ::im crate features a ::im-rc counterpart with, again, a duck-typed API to allow easily choosing the flavor of thread-safety that users want, while also being friendly / compatible with complex Cargo dependency trees (compared to using features to choose the flavor, for instance; at the cost of a bit more binary bloat, though).

    • Until const fn for traits is featured, or async fn for those not wanting or being able to use heap-allocations, some crates out there (ab)use duck-typed APIs to feature similar behavior. As of this date, ::const_format is a very clever real-life example of this.

8 Likes

Thanks all for the great insights into duck typing.

1 Like

you can sort of do duck typing with specialization:

#![feature(specialization)]

trait Quack {
    fn quack(&self);
}

// if we add `T: ?Sized` the compiler panics
impl<T> Quack for T {
    default fn quack(&self) {
        panic!("`Quack` not implemented for {}", std::any::type_name::<T>())
    }
}

struct Duck;

impl Quack for Duck {
    fn quack(&self) {
        println!("quack!")
    }
}

// NOTE: no `where T: Quack`
fn quack_in_a_loop<T>(q: &T, n: usize) {
    for _ in 0..n {
        q.quack()
    }
}

fn main() {
    quack_in_a_loop(&Duck, 3);
    quack_in_a_loop(&0, 5);
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=6cb4b8602323a0151c288d1e564fa88d

2 Likes