DEV Community

ProgramCrafter
ProgramCrafter

Posted on

Somewhat dynamic typing in Rust - magic of traits

Rust is statically typed language, they say. All the types are known at compilation time, they say.

That's not quite true, and I'd like to demonstrate counterexamples!

Functions working on slices (arrays of unknown size)

fn foo(arr: &mut [usize]) {
    arr[arr.len() - 1] += arr[0];
}

fn main() {
    let mut v: [usize; 4] = [7, 5, 1, 3];
    foo(&mut v);
    println!("{v:?}");    //  [7, 5, 1, 10]
}
Enter fullscreen mode Exit fullscreen mode

As an example, I'll use a function that adds value of first array element to the last one. Note that it takes &mut [usize] - thing called "slice", which doesn't have length indication at all!

If you change length of array v (suitably adding or removing elements in brackets), function foo will be able to process it just as well. Thus, it can be said that foo works on values of different types!

Functions on trait objects

use std::any::Any;

fn foo(something: &dyn Any) -> &'static str {
    if something.is::<char>() {"char"} else {"not-char"}
}

fn main() {
    println!("1: {}", foo(&1));       //  1: not-char
    println!("'k': {}", foo(&'k'));   //  'k': char
}
Enter fullscreen mode Exit fullscreen mode

Any is an interesting Rust trait, implemented by almost all types. It allows you to store references to values of different type in a single container, and then to check if value has a certain type.

Why not all types implement Any?
Any depends on all types having unique IDs. References (and, by extension, types containing references) are bounded by lifetimes, which are hard to put into numbers.

But perhaps the most interesting fact is that you can pass reference to a certain object (for instance, 1 of usize) to foo, it will automatically convert to reference to dyn Any, and the code will compile!

What is '&dyn Any', is there '&Any' as well?
dyn Trait represents an object of some type - unknown at compilation time, but certainly implementing Trait. Those objects may have different sizes and thus cannot be stored normally (passed to function or returned, as well). They must be behind a "smart pointer" - reference or Box, for instance.

There is also impl Trait, representing object of type known at compilation time that implements Trait as well. They may be used without restrictions.

fn maybe_max(a: impl Into<usize>, b: impl Into<usize>) -> usize {
    std::cmp::max(a.into(), b.into())
}
Enter fullscreen mode Exit fullscreen mode

In next post, I'll describe how Rust works with this dynamic typing in more details!

Top comments (0)