Why read_lines<P>(filename: P)?

HI All,
I read 'the book' a few months ago and finally have time to get back to learning rust by working on a small example project which starts by readng a file.

Following the code in rust by example I see this method defined:

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {

I don't understand this method signature and why it's using a generic rather than a parameter usch as this:

fn read_lines2(filename: &Path)-> io::Result<io::Lines<io::BufReader<File>>>

Having tried the latter, it works the same, so, why does the example use the generic approach? Is that a generic or have I misunderstood something else too?

Thanks in advance!
Tony

It's mostly because the generic version lets you pass a &str directly, since str implements AsRef<Path>.

1 Like

Thank you, wow, that was a fast reply.
I don't quite follow, how and why is that different to passing &Path ?

Suppose you want to read lines from a file named foo.txt. Since a string literal &str is not the same type as a &Path, you'd have to convert your filename explicitly using the AsRef<Path> implementation:

let lines = read_lines2("foo.txt".as_ref())?;

With the generic version, you can pass anything as an argument that implements AsRef<Path> and the function will take care of calling .as_ref() for you, so you can write simply

let lines = read_lines("foo.txt")?;

(Technically this is using the AsRef<Path> impl for &str, which comes from this blanket impl, rather than the one for str.)

1 Like

I think I get you know, still trying to get used to Rust, so why wouldn't this work?
The error message is that filename doesn't have a szie known at compile-time. This is a great feature of Rust, but will take some getting used to coming from a c# background.

fn read_lines2(filename: AsRef<Path>)

You can't do that because AsRef<Path> is a trait, not a type.

4 Likes

Another way to write this is:

fn read_lines2(filename: impl AsRef<Path>)
1 Like

Unlike languages such as C# and Go, an interface doesn't actually exist at runtime so you can't pass around a bare trait like that in Rust. A trait is just a contract which says a type will have a particular set of behaviour.

That means you need to either make your code generic (fn read_lines<P>(path: P) where P: AsRef<Path> or fn read_lines(path: impl AsRef<Path>)) or use something called a "trait object" (written as dyn AsRef<Path>) which is a special struct that the compiler generates for doing dynamic dispatch and must always be behind some level of indirection.

The underlying reason for this P: AsRef<Path> generic's existence is to paper over the complete mess that is filesystem paths and "strings" across different operating systems. By accepting any P which implements AsRef<Path> your read_lines() function can be given anything that is convertible to a path and it'll Just Work.

It's really convenient to use Rust's UTF-8 strings and string literals (String and &str) for dealing with the file system, however it turns out that UTF-8 is too strict and both unices and Windows can support a larger (incompatible) range of "strings". We don't want to make certain things inaccessible so we need a way to express this.

The OsString and OsStr types exist to represent strings which are valid on the current OS but possibly not valid Rust strings, and the PathBuf and Path types exist as strongly-typed wrappers around their OsStr counterparts so you can have nice methods like my_path.exists() and my_path.join("folder"). Path and OsStr can't be combined into a single type though, because you use strings for more than just filesystem paths when talking to the OS (e.g. environment variables are OsStrings).

This is one example of where Rust has been bitten by this in the past:

There's also CString for working with C's null-terminated "array of bytes" strings, but let's not talk about that.

5 Likes

Nitpick: TypeScript is not a good example, since interface doesn't exist there too - we can use it as a type due to (a kind of) duck typing, not due to it being something really existing.

3 Likes

Good point. I was mostly thinking about how interface types are first-class citizens and can be passed around or named directly, and completely forgot that most of TypeScript is smokes and mirrors on top of JavaScript's loose type system.

In other languages AsRef<Path> does not exist. Their "Path-like" abstract types are equivalent of Rust's Box<dyn AsRef<Path>>. fn read_lines(path: Box<dyn AsRef<Path>>) would work, but Rust wants to be more efficient than this and avoid allocation and dynamic dispatch.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.