Today we going to explore Polymorphism and how that is actually doable in Rust.
For those that have not done any OOP language yet, Polymorphism is just a fancy term for behaving or being polymorph. For example a instance of something can also be or behave as something else. In Java, you would use interfaces
to declare some behaviour that can be implemented by a class. So one class can implement one or many interfaces. In rust there are only structs no classes and traits that act as interfaces. As simple as that.
the problem statement
We want to implement a very basic hexdump tool that either accepts a file as argument or reads from stdin
if that file is not provided.
for instance usable like this
hexdump README.md
0000000 23 20 48 65 78 64 75 6d 70 0a 0a 23 23 20 55 73
0000010 61 67 65 0a 0a 60 60 60 62 61 73 68 0a 68 65 78
0000020 64 75 6d 70 20 52 45 41 44 4d 45 2e 6d 64 0a 0a
0000030 60 60 60 0a
or without a file
hexdump
foo bar bak^D
0000000 66 6f 6f 20 62 61 72 20 62 61 6b 0a
the basics
Q: How to open a file
A: File::open(file)
Q: How to read from a file
A: BufReader::new(File::open(file))
Q: How to read from stdin
A: stdin().lock()
explanation
So it seems that stdin().lock()
and BufReader::new()
seems to have something in common that. The both allow to read bytes
from them. Well, we come to this in a moment. Let's dump the full code for now:
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::io::stdin;
use std::io::BufReader;
fn main() -> std::io::Result<()> {
let args: Vec<String> = env::args().collect();
match args.get(1) {
Some(file) => read_and_dump(BufReader::new(File::open(file)?)),
None => read_and_dump(stdin().lock()),
// ^--- this function will be implemented later on, don't worry
}
Ok(())
}
the trait BufRead
So the thing that both (stdin().lock()
and BufReader::new()
) have in common, the both implement the trait BufRead
at the declaration we can see pub trait BufRead: Read
that means that the BufRead
extends Read
by some more methods.
the naive approach
Perfect so let's now implement the missing method from above and hand over the instance that implements the BufRead
fn read_and_dump(r: BufRead) {
for (i, b) in r.bytes().enumerate() {
print_line_no_once(i); // <-- we'll come to that later
print_byte(b.unwrap()); // <-- we'll come to that later
}
print!("\n");
}
Cool, let's ask the compiler how he likes it..
cargo build --release
Compiling hexdump v0.1.0
warning: trait objects without an explicit `dyn` are deprecated
--> src/main.rs:17:21
|
17 | fn read_and_dump(r: BufRead) {
| ^^^^^^^ help: use `dyn`: `dyn BufRead`
|
= note: #[warn(bare_trait_objects)] on by default
[...]
error[E0277]: the size for values of type `(dyn std::io::BufRead + 'static)` cannot be known at compilation time
--> src/main.rs:17:18
|
17 | fn read_and_dump(r: BufRead) {
| ^ doesn't have a size known at compile-time
|
= help: the trait `std::marker::Sized` is not implemented for `(dyn std::io::BufRead + 'static)`
= note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
= note: all local variables must have a statically known size
= help: unsized locals are gated as an unstable feature
well, that would be too easy.
It took me actually a bit time to gasp the problem, I read the docs and not got much further until I realized that traits should not be used as direct types but rather as "marker".
To explain what I mean with "marker" let's checkout the next iteration
Version 1: the impl on anonymous type
fn read_and_dump(r: impl BufRead) {
for (i, b) in r.bytes().enumerate() {
print_line_no_once(i);
print_byte(b.unwrap());
}
print!("\n");
}
So here we not name any type and just say that whatever type we provide it will implement the trait BufRead
. And by doing so the compiler is perfectly fine with everything.
Version 2: generic short hand
I was curious if generics are not as well capable of solving this. Just with the difference that the compiler might be able to do further optimizations.
fn read_and_dump<T: BufRead>(r: T) {
for (i, b) in r.bytes().enumerate() {
print_line_no_once(i);
print_byte(b.unwrap());
}
print!("\n");
}
so in <>
we specify a type T
that implements BufRead
that is called a bound generic in rust.
Version 3: generic verbose
When you implement traits you will see that syntax quite often, so it is worth to mentioning the where syntax
fn read_and_dump<T>(r: T)
where
T: BufRead
{
for (i, b) in r.bytes().enumerate() {
print_line_no_once(i);
print_byte(b.unwrap());
}
print!("\n");
}
Here we name the bounds of the generic by the word where
directly before the body {}
braces. More than just one can be appended by ,
.
wrap up
to not miss out the other functions, even thou they are not showing much here you go:
fn print_line_no_once(i: usize) {
if i % 0x10 == 0 {
if i > 0 {
print!("\n");
}
print!("{:07x}", i);
}
}
fn print_byte(b: u8) {
print!(" {:02x}", b);
}
So we have now learned how we can use anonymous types or generics to make use of loosely couple our functions to traits instead of tightly couple them to the real structs that they are. That is also sometimes referred to as the Open Close Principle
references
Further reading about subtle differences can be found on reddit
the title image belongs to Blizzard Entertainment and is available under CC BY-NC-SA 3.0.
used versions:
$ rustc --version && cargo --version
rustc 1.37.0 (eae3437df 2019-08-13)
cargo 1.37.0 (9edd08916 2019-08-02)
Please don't forget to share your feedback, and let me know what was your learning if there was any. If you have more variants that I missed, please share them.
Top comments (0)