What's New in Rust 1.40
Episode Page with Show NotesJon Gjengset: Hello, Ben.
Ben Striegel: Hello, Jon. Happy New Year.
Jon: We are back yet again, believe it or not. I think we have to start off by giving a bit of an apology to our listeners, because we’re going to talk about Rust 1.40, and it came out December 19th. And instead of just delaying by a few weeks, as we usually do, we decided not to release the next episode until the next decade. It’s a very long time.
Ben: I think our listeners will forgive us, hopefully. But, I mean, it was a prudent choice, really.
Jon: That’s true. It was— we were just very busy. All right, Ben, let’s dig
into the release notes for 1.40 and I guess at the top of that list is the
non_exhaustive
attribute.
Ben: Yeah.
Jon: So this attribute is really cool, especially if you’re a library
author, because it lets you annotate a type with the non_exhaustive
attribute.
And what that means is that you intend to add things to this type later. So if
that is a struct, that means that you might add fields to it later. Or if it’s
an enum, you might add variants to it later. And this is really handy because it
means that you can take— you can expose publicly a type that you know is going
to change in the future, in such a way that it’s going to be backwards-
compatible if you add things later.
So the trivial example of this is, you have an error type, like, an error enum,
and you have a bunch of variants for the different errors, and you want the
ability to add more error types later, without breaking people’s code. And what
non_exhaustive
does is one primary thing. It means that no one can match on
your type, without including, sort of, the underscore pattern, right? They need—
when they do the match, they always need to write the code under the assumption
that more things might be added. And there are a lot of places where this makes
sense. So one case is, let’s say you have a struct and some of the fields of
that struct, you want to make public. Normally, this would be a bit of a
problem, because if you did that, people couldn’t— and then people started
matching on your struct, for example. Then there wasn’t really a way for you to
add more public fields later without breaking people’s code if they relied on
your type.
Ben: I want to kind of back up, though, because, like, in this sense, talk
about matching, or any kind of pattern usage. Normally we think about enums,
right? Like, it’s— if you ever use an enum in Rust, pretty much the way that you
have to use it, one way or the other, is you have to match on it. And sometimes
there are convenience methods on Option
. That means you can just write
is_some
as opposed to have it going through a whole match expression. But,
like, so you are probably familiar with, you know, the match
, you say, first
pattern, second pattern, then underscore, return this, as the default case, kind
of analogous to a switch statement in other languages, where you have a default
case at the very bottom, I think for structs, it’s a less well known fact that
you can match on structs, or that you can use a struct in a pattern context. So
why don’t you talk about, like, where structs are currently usable in pattern
contexts and why you might want to do that.
Jon: Sure. So the most common case for this is, you have a struct of some
type, and you want to look at just one field of that struct. And one way that
you can do that is, just— you have a type you have a variable x
of type foo
,
and you do something like x.field
, right, and that is one way to get at that
that field of that struct. But there are a bunch of other times where you might
want to— let’s say, do a nested pattern match, right? So you want to look at a
foo
where it’s a
is a bar
where the bar
s b
is something else, right?
So you want to construct this nested pattern and you want to get, say, a
reference to some field that’s deeper down in that pattern. And maybe there’s
some enums or variants in between. And so it ends up being a pattern that’s not
just a direct field access.
And in the past, as a library author, you sort of just have the choice of,
either I make this field public, and then let people access it. But then if I
add a field later, anyone who matches on it, that pattern is no longer going to
compile, because it doesn’t mention this new field that I’ve added. So adding a
public field would be a non-backwards-compatible change. Right? If I had a type
foo
that had only one public field a
, then previously— or in the current
version, people could match on foo
and then just write, sort of, { a }
, and
then be able to access that field a
. But if I add a public field b
now, then
that code that someone wrote with the current version of my library would no
longer compile, because the compiler would say, this pattern doesn’t match foo
because foo
has two fields, a
and b
. And so the way that you do pattern
matching for these non_exhaustive
types, or in general, when you do pattern
matching the way that you should probably do it, is to include, for structs at
least, the sort of , ..
pattern. So this is saying “I’m going to match on
these fields, and there are others that I want to ignore.”
Ben: Is that actually— does that work today? The , ..
pattern?
Jon: Yes, it does indeed. So this is a really common thing to use, even just
internally in your code base, where— let’s say you want references to two of the
fields of Foo
and you don’t care about the others. You could write that as,
like, Foo { field1, field2,
and then match on all the other fields as well,
and then, just map them to underscore something. But that means that if you
later changed Foo
in your code base, you would have to update all of the
pieces where Foo
is matched on, and that’s really annoying. And so instead, in
the pattern you just write Foo { field1, field2, .. }
and that means, there
are other fields, ignore them. And so, that already works on stable today. What
this change is letting you do is, mark your type as something where the compiler
is going to enforce that people who match on your type must use the , ..
pattern
Ben: Or for an enum, the _ =>
pattern.
Jon: Right, exactly. They must assume that there might be more things, in whatever pattern they write. And so this means that you can now add more variants, and know that you won’t— you won’t make users’ code not compile, just because you added a field or a variant.
Ben: Now I think it’s worth talking about here that there is a tradeoff, in that some users, in fact, do want their code to stop compiling when one of their dependencies adds a new field to some enum, and kind of the whole point of a match being exhaustive, in some ways, is that you can do this. And so the default behavior is still that that happens, obviously. But is it like, maybe, a thing to use with caution, this new attribute? Just because some users really enjoy the idea of, okay, I’ve covered every single case and nothing can go wrong. They don’t need a default case, or like, a fall-through backup case.
Jon: I think that’s definitely true, both for structs and for enums. But
perhaps especially for enums, where this sort of— the compiler checking that
you’re matching exhaustively is really handy. I think usually what people will
end up doing, is have the underscore match map to, like, panic or something. But
that just means that now you’re going to get runtime errors, which is something
we want to avoid. And so in libraries, only use this if you really need it. If
there’s a type where you expect that you will add more things, and you don’t
expect that people are matching exhaustively on it. There are libraries today
that try to hack around this by adding, like, a variant, that is called
__non_exhaustive
or something like that, and it works, but it’s not very
pretty. And that does give users the option of exhaustively matching. But it
does mean that you you might break semantic versioning accidentally, in some
sense.
Ben: Yeah, I’ve seen, I think the url
crate is very popular, obviously,
has a version of this where it has, like, one variant, where it’s, you know,
it’s there, you can use it, but it’s underscore underscore something un-
guessable name, and then it’s marked #[doc(hidden)]
so it doesn’t actually
show up in documentation. So you have to actually go read the code to know that
it’s there. It’s kind of like, hey, we want to be able to evolve this crate in a
way that doesn’t necessarily break our users down the line. Because for a crate
as fundamental as url
, that’s a big deal. Like it has, may have— one breaking
release in the past. And it was a huge problem for everyone involved. So in this
case, I think really, the real guideline here is just, errors are a great place
to use this attribute. Maybe not really anywhere else. Errors you might expect,
hey, like, I want to be able to have my crate, you know, throw a new kind of
error, you know, return a new kind of error, like maybe internally, your policy
is that, you know, for every— each of the crates that your crate uses, its own
dependencies, maybe you have, like, one kind of wrapped error for whatever it’s
doing, and you don’t want to, like, oh, if I add a new crate, I can’t now— and
it has its own error type, what am I going to do with my custom error type?
Like, I can’t add a new variant to wrap that type, which is a very common
pattern and how I really enjoy modeling my error handling in my Rust crates.
Jon: Yeah, I think that’s true. And especially for errors, it’s rare that you want to exhaustively match on them. Usually, what you want to do is, like, if it’s one of these two errors or three years, then I want to handle it, otherwise, I want to, like, just bubble it up with question mark or something.
Ben: Yeah, which is why I think that, like, yeah, it make sense here, in errors specifically.
Jon: Yeah, And I think even in the standard library, the
std::io::ErrorKind
works a little bit like this, where I believe that that
either has, now, the non_exhaustive
flags or there’s, like, a hidden variant
that you can’t match on.
Ben: Yeah, it’s actually— it’s a pretty useful thing, and I think that I want to, like, talk about real briefly, just how there are some languages that really go the distance, in terms of really enabling libraries to evolve, like adding language features that then enable libraries to change, without having to necessarily impose breaking changes on their users. And I think the biggest language I’ve seen do this is Swift, which has a whole lot of features that are, like— it’s kind of— if you were to just read the feature list, you’d be like, when is this actually ever useful? And eventually at some point it clicks where, hey, they want to be able to change the underlying libraries, without forcing end users to have to make changes to their code. Because they want to be able to, like, shift a stable, swift runtime and library with all the Apple devices. And then have it just work, pretty much forever.
Jon: Yeah, I think in some sense this is, like, to do in the favor of building a robust ecosystem. Right? Where you want breaking changes to happen as rarely as they can.
Ben: And I think the poster child of this, kind of, idea of languages supporting features that enable libraries to change backwards compatibly, is probably something like, default arguments in functions because you can imagine like, oh, I have this function. And now people are using it. Oh, but I want to add a new parameter to it. And I can’t do it without making people change their things— oh, wait. If it has a default, then no one needs to change anything. So it’ll be a— it’s kind of a topic people have wanted for a long time in Rust, the idea of default function arguments, or keyword arguments. So who knows? Maybe we’ll eventually evolve in that direction.
Jon: Maybe one day. There have been a bunch of other changes in 1.40, and I think a large swath of them fall into the category of various macro improvements. And this is an area that we’ve touched on in the past, of how there have been changes to the Rust compiler up through the releases, that seem to be targeted primarily at macro authors. And it’s the same in 1.40, although here it seems like the focus is a little bit more on the use of macros. So the first of this, is that you can now call function-like procedural macros in type context.
Ben: Back up. What is a function-like procedural macro?
Jon: Yeah, there are a lot of words to unpack here. So, procedural macros, for those who aren’t aware of them, are basically Rust programs or functions that take as input Rust code, and produce as output Rust code. And this lets you write macros is in a somewhat convenient way, right? You get a token stream of input, which is just the Rust code that was provided to the macro. And then you produce Rust code as a token stream, which is going to be the code that actually gets compiled. And then there’s some— the function basically gets to juggle the incoming token stream around to produce the desired output code.
And a function-like macro is one that you call a little bit like a function. So
these are the things that look like some name exclamation mark and then some
open brackets and a bunch of arguments. There are other type of macros that are
not function-like and these would be things like if you see a #[ ]
in front of
a function or a type or something, so this would be stuff like #[derive( )]
.
It might be things like— the various attributes you can add when you’re using
serde
serialization and deserialization. They can be other things, like
non_exhaustive
arguably is a macro attribute, although it isn’t actually
implemented that way. And so there are a bunch of different types of macros. But
the function-like ones are the ones you’re most familiar with that have the
bang, and are the kind of things that are generated if you use, for example,
macro_rules!
. And what changed— the first thing here that changed is that now
you can call those function-like procedural macros in a type context. So that
is, you can write a function-like macro that produces a type signature. So, for
example, the example they give in the release notes is, you say something like
type Foo =
and then a call to a macro that’s going to expand to some type. And
this is something that you couldn’t do in the past.
Ben: I could also imagine, maybe a macro like this being used with, like,
mem::size_of
, perhaps. If you wanted to try to think about, like— because
right now, you could still use, like, a macro_rules
macro in this type
context, trying to think of a thing that might be useful for a procedural macro
specifically. And so say, if you had— there are some crates out there that use
procedural macros to do really wild things, like, check certain database schemas
at compile time, for you to verify that your SQL is correct. And you could
imagine maybe something like that, creating a type, and then you might want to
check the size of that, to see if it does certain things. So I think it’s mostly
the use for— kind of like one flagship use for this.
Jon: Yeah, I think that would be one example. And the other is the recurring one that that we’ve mentioned before, which is this of having macros, in some sense, not be special. That anywhere where you can write Rust code, you can just swap in a macro and have it work. So in some sense, it might just be for the purposes of completeness, right? To remove rules around where you cannot use macros. Previously, you could couldn’t use a macro in that position, and now you can.
Ben: Although I think it’s worth talking about, what places can you still
not use these sorts of procedural macros? So prior to this release, you could
only use function-like procedural macros in what’s called item position, which
is basically— item position is just like, look where you’d usually define your
structs and your statics and your consts, and all these things in your code,
it’s the very top level. So an item is something that exists, just like, not in
expression position, inside like a function, say. That sort of thing. It’s a
definition that you would see anywhere normally in your code. And now you can do
it in type context, but, for example, you still can’t do it in expression
context. And so for example, the println!
macro, which is pretty much a
function-like procedural macro— it’s a compiler built-in, but we’ll ignore that,
for now— is usable, just in a normal expression. And so it is pretty much
useless to actually do this, because the println!
macro just returns nil.
Jon: Well, the format!
macro might be a better example.
Ben: There you go. So it returns a String
. And so the format!
macro is
not a thing you could write with macro_rules!
. It is— actual interpretation of
strings at compile time, inspects the interior, type-checks it, and in this
case, then returns a String
, and so you can use it as part of an expression.
You can say let x = format!(something)
and it’s incredibly useful. You can’t
currently write your own procedural macros in this way, but there is a crate by
the inimitable David Tolnay, author of serde
, or I guess maintainer and author
of serde
, who— the crate is called proc_macro_hack
, which does allow you to
write procedural macros in these positions by effectively just defining for you
a macro_rules!
macro, which can be used currently, is both of those positions,
and then having an internal crate— a separate extra internal crate, then it
exposes different things. So, it shouldn’t be necessary, but right now it is
possible to do, if you want it hard enough.
Jon: Yeah, I think it’s interesting too, to observe that macro_rules!
is
still fairly different from procedural macros. Right? A function-like macro
that’s defined through macro_rules!
, you can use in a bunch more places than
you can function-like procedural macros, and I don’t really know why that is.
But it’s interesting that it’s the case. Although I think they are trying to
remove those differences as much as possible. And this is sort of a first step
towards that.
Ben: Yeah, making things more uniform is certainly a nice simplification. We
don’t want to have to explain, why does this work here? This doesn’t work here.
Speaking of that, that’s a good segue into our next macro-related change. Now,
all kinds of macros are usable within extern
blocks, and so both
macro_rules!
, macros and procedural macros. And so, an extern
block is, if
you’ve ever used extern "C" fn
before in Rust, as a way of defining the
signature of a C function. extern
blocks are a way of defining those kind of
signatures, for many functions at once, as a convenient way, and for whatever
reason, which I have— I don’t understand right now, I need to actually ask—
macro_rules!
macros wouldn’t expand within these blocks. Maybe it was just
being a defensive kind of thing. But as of 1.40, macro_rules!
macros now can
be used inside of here. And you can also do things like put attributes on these
extern
fn
definitions. So that’s, again, just another place in Rust code
that is no longer special, things should just work as you expect. So that’s a
nice change.
Jon: I think the next one falls in a very similar category too, of,
previously, you couldn’t use macro_rules!
to define a macro inside the output
of a procedural macro, whereas now you can. Right? It’s just another instance
of, like, this wasn’t possible before. And this is a place where macros had to
be treated specialty. And now that is no longer the case.
This last one, though, I think, is interesting, because it’s a little bit odd
when you first look at it. So when you use macro_rules!
to define a macro that
takes parameters, you can say what the type of those parameters should be, and
the types here are a little bit different from normal, like, function parameter
types, in that they are more types of expressions, rather than actual Rust
types. So, for example, you can say that the first argument to a Rust macro
should be a path to a type, or an expression, or an entire token tree, or a
constant. So these are sort of, if you will, meta-level Rust constructs. You can
say, I want this type of syntax, in this place, of a parameter for my macro, and
one of those types is the meta
type. The meta
matcher, as it’s called. And
the meta
matcher matches anything that looks like a attribute-style macro. So
these would be things like derive
s, for example, or the serde::Serialize
attribute macros we talked about before, and the change that’s happened here is
that now that matcher accepts basically arbitrary sequences of tokens. So that
is, you can— if you use that matcher, it will now accept basically arbitrary
Rust code.
And the reason for this change is a little interesting. Originally, the meta
matcher was there because you wanted something that matched the syntax of these
attribute-like macros, but over time as these types of macros have developed,
the Rust developers have decided that they basically want to allow almost
arbitrary syntax to be used in those attribute macros. Previously it would just
like, #[path(attribute)]
. Whereas now, they’re leaning more
towards, you should allow any kind of Rust syntax in there. But that also means
that the meta
matcher becomes sort of useless, right? Because it has to match
basically anything. And at that point, they had the option either of removing
the meta
matcher or just to make it support parsing arbitrary— or just accept
arbitrary sequences of tokens. The problem is that it’s— for much of the
standard library, they can deprecate methods and types just fine. But a matcher,
it’s not entirely clear how you deprecate. There isn’t a mechanism in Rust to
say that this value for a part of the syntax is now deprecated. It’s not really
something that Rust has a story for, in terms of its backwards compatibility. It
might be something that they could, like, remove the meta
matcher in the next
edition, maybe. But they don’t have a good way, at least for now, to even
indicate that it’s been deprecated. And so the solution was to make the meta
matcher just now support arbitrary Rust token streams. And so that’s what it
does.
Ben: So those are all of our macro changes. And it is interesting to think about, where it’s something that— it’s become so useful in other ways, the interior of attribute macros that it’s now— this feature designed to accommodate them is now too broad, and isn’t really good for anything any more. But it’s a good thing.
Jon: Yeah, exactly.
Ben: It means that now attribute macro bodies are no longer special, they’re no longer unique. And in the future, maybe we won’t need to even have this feature of the language anymore.
Jon: Yeah, and I think if you’re— if people are curious about, sort of, some of the the internal discussion and process for making these decisions, looking into how that particular change ended up landing in the way that it did gives some useful insights, it’s worth reading up on.
Ben: So this next one here, borrow check migration warnings have become hard errors in Rust 2015, and I think we have spent, I think, maybe, two or three previous episodes talking in length about what this means for Rust 2018, and Rust 2015, and for the compiler, and for the borrow checker. And so I would recommend you just go listen to those episodes. I think 1.39 had some, I think 1.36, maybe 1.38, possibly. It’s just— we’ve been over this before. If you’re on Rust 2015 then you need to update your code to accommodate the new borrow checker that came last year. And I think it’s all we’ll say about that in this episode.
Jon: I think that’s reasonable.
Ben: We have spent a lot of time. Yeah. Oh, ongoing constification, making
more standard library functions and methods const
, usable in constant
contexts. We really should have some kind of, like, dedicated musical interlude
intro, because this happens every single release.
Jon: It’s true.
Ben: It’s a recurring segment. The is_power_of_two
function in the
standard library, which tells you whether or not an integer, is or is not a
power of two, is now usable in constant contexts.
Jon: Is that what it does?
Ben: Yeah, yeah. It’s kind of subtle, because you couldn’t tell.
Jon: We should probably have an episode on that function.
Ben: Yeah, we could do maybe three or four. And in this case, something more
interesting, perhaps, that I wanted to talk about is that, last episode we
mentioned in passing how there was, at that moment, a PR to the Rust compiler to
enable if
and match
expressions to work in constant contexts. And this is a
big deal, because right now, that’s kind of the major thing that’s missing.
Aside from, say, looping. But if if
and match
expressions were to work in a
constant context, it would kind of unblock everything and, kind of like, unleash
the floodgates for a whole lot more things to become const fn
s. And so, in
this case, that PR has since landed in the past six weeks.
And, previously, in order to make certain things const fn
s in the standard
library, we had to, kind of, make concessions to readability. And because a lot
of what was supported previously in const fn
was mostly bitwise operations, a
lot of— in order to make things const, a lot of these const fn
s kind of became
more unreadable as a result of taking out branches or match statements in order
to make things work in constant context using only bitwise operations. And that
was always all of— any time that happened, it was marked with a certain tag in
the issue tracker called
“const fn hack.”
And so now that if
and match
, have
become— not stable yet, but they are in nightly and usable— the “const fn hacks”
have been removed. So if you are using nightly Rust, you are in fact using
actual if
and match
const
.
Jon: I think this is a good place to also plug your RustFest episode, or ours, I should say. RustFest episode where you talk to Oli.
Ben: Oh, yes. So, Oli— I forget his last name. “oli-obk” is how I know him. But it was the most recent episode, and I talked to him at RustFest 2019, last November. And it’s a great, just, talk on, hey, what is, like, how does the Rust compiler do constant evaluation? Like, what’s coming in the future? What is the underlying tool, called Miri, how cool is it? The answer is very. I don’t want to spoil it. But yeah, you should definitely go take a listen to that. It’s not super long. About half an hour. And we have more interviews on the way, so stay tuned for that.
Jon: I’m very excited. Another thing I’m very excited for— what a segue— is.
The todo!
macro, which, this has been— there’s been a lot of debate around
this, and whether it’s, like, useful. And that is, in Rust 1.40, the Rust
standard library gained a new macro called todo!
. And todo!
, much like
unimplemented!
and unreachable!
, is really just a synonym for panic!
with
a custom message. And in the release notes, it’s mentioned as a shorter, more
memorable, and convenient version of unimplemented!
.
Ben: So, actually, I love this macro. I love unimplemented!
in the first
place. I use it all the time in Rust development. If you aren’t using this
macro, like, you need to get on this, you need to get on the train. What I
normally do is, if I’m, like, making a big change, or making a huge new
addition, what you normally want to do, is you want to figure out— I’m not gonna
impose, what I want to do normally, is I want to figure out all the type
signatures first, I want to get everything into place and then start, like,
implementing things one at a time, piecemeal. I want things to first start type-
checking. Even if they panic at run time, just as kind of a basic case for
knowing what I want to be doing. And so I’ll write a function signature. Maybe
I’ll have, like, some new type that wants to implement a dozen traits. And all
those traits have their own functions. And so, rather than having to go through
and do all those function bodies upfront to get them to actually type-check, I
will just write unimplemented!
and it will just type-check. And this works
because of the “never” type and diverging functions, and the bottom type, if you
want to call it that. Let’s not get into it. The point is that any function that
panics just type-checks, because panicking is always a valid, quote-unquote,
return type, because it isn’t a return type.
And todo!
is great. Just being shorter to type. If you’re using, like, VS
Code, it doesn’t actually matter, because, type uni
and then “tab” will
automatically autocomplete it for you. But actually, I think the reason that
this warrants inclusion is that unimplemented!
kind of expresses something a
bit different in the mind of a reader. And so we have the unreachable!
macro,
and the unreachable!
macro says, hey, I can’t prove it to the compiler, but
trust me, the author, the code’s never going to get to this point. And so, say,
for example, you have let X = Some(42)
, like as an Option
, and then later
on, you have to then do some operation where it’s like, hey, you have an
Option
here. Do you know it’s a Some
? And there’s no way to say to Rust
today, hey, like, up here, if you just look, here is where I make it a Some
,
and it’s guaranteed to be a Some
at this point in the code. So you just have
to say unwrap
or something. And if you were to instead use a match
, you
could say unreachable!
on the None
case, because you have proven to
yourself, and to readers, hopefully with the aid of a comment, that hey, like,
this can’t ever be taken, just look up here, like, there’s no actual point at
which this branch ever be taken.
Jon: Right. In some sense unreachable!
is like a piece of self-documenting
code, so to speak.
Ben: And obviously, you could at some point get it wrong. Maybe in the
future it changes. This is why it still has to panic at the end; Rust has to be
conservative. There is, in fact, an unsafe version of unreachable
in the
standard library, if you know for sure that— you are more certain, and you don’t
want some kind of performance hit, or potential branch that won’t ever be taken,
to be generated in your code. But that’s that’s off-topic. And then
unimplemented!
is the version of that saying, hey, like, we haven’t
implemented this, and then this kind of— in the past, this implicit assumption
that hey, we will implement it in the future. But it turns out that it’s
actually useful sometimes to be able to say, hey, like, I haven’t implemented
this and I don’t intend to ever do that. And so, that’s kind of what in the
future we’re moving forward with with having this split between todo!
and
unimplemented!
, where you can use unimplemented!
to say, hey, I don’t intend
to ever implement this. If you want it, too bad. Do it yourself, fork my
library. Whereas todo!
macro is more for saying hey, like, I will do this in
the future. Just that right now I haven’t gotten around to it.
And so, for example, in my work, when I work on Rust stuff, I might send a
tentative draft PR, which just have, like, a billion todo!
s, stubbed out. And
the first time my boss— remember in the past, it was unimplemented. And first
time my boss saw this, he was like, Do you actually want to, like, write this
code? And I’m like, no, no, no, I will. I’m just trying to say like, hey, take a
look at it. What do you think of this general structure? That kind of thing. And
so this has actually been to me where people have looked at my unimplemented!
and said, hey, do you— shouldn’t you be doing this? And just doesn’t really
express the idea of “I will be doing this. I just haven’t yet.”” And so todo!
for me is great for that reason. Or it just says, to any potential readers or
reviewers, hey, like, this will be done. Just not right now.
Jon: Yeah, it’s interesting. I think people also use these in different
ways, right? Like for me, unimplemented!
is something that I know I have to
fix before this code will be done. Whereas todo!
is, I know this has to be
fixed at some point, right? So people use it a little bit different. And I think
part of the discussion that was happening when todo!
was proposed, and then
added was, should the standard library really have a macro for all of these
different use cases? And some people say todo!
is common enough and different
enough that we should, and some people thought, well, sort of, this is— almost
like a slippery slope argument, right? Like how many macros are we going to end
up with? I think todo!
is probably worthwhile to add, but there was a lot of
discussion, and also discussion about “if we add todo!
, should we deprecate
unimplemented!
?” I think ultimately the decision was to keep both, and I think
that’s pretty reasonable. At least now that people know that both of them exist.
There are also been a bunch of other additions to the standard library, some
small and some large. One of them is the slice::repeat
function, which is
really straightforward and pretty handy. So previously, we’ve talked about the
repeat
function, which lets you create a vector that just is a bunch of
instances of one type. And now the slice
module has also gotten a repeat
function, and this lets you take a slice of type T
and repeat it N times and
then collect the result into a vector. And so this is just a handy way to, just
sort of, repeat a sequence of things multiple times.
The other thing that’s under the standard library is mem::take
. So this in the
mem
module, and the take
function is really just a convenience wrapper
around the mem::replace
function. So mem::replace
takes a mutable reference
to a T
, and a T
and then it swaps those two, and then returns you the thing
that the mutable reference was pointing to. So if you have some variable x
that you have, you just have a mutable reference to. And you want to replace its
entire value with a new value. So this might be, for example, you have a
HashMap
or you have a mutable reference to a HashMap
, and you want to, sort
of, take all of the values in that HashMap
and leave an empty HashMap
in its
place. Then previously, you could do mem::replace
, give it the mutable
reference to the map, and then have the second argument be HashMap::new()
. Or,
if you will, if you have a type that implements Default
, you could write
HashMap::default()
or Default::default()
. And mem::take
is just a handy
shortcut for this, where you can say std::mem::take
and then give a mutable
reference to, say, a HashMap
. And what that’s going to do is it’s going to
stick a default hash map, so an empty one, in the place of where the mutable
reference is pointing to, and then it’s gonna take
the old map and give you an
owned reference to that map, so it just swaps something with its default
implementation value. This can be handy, especially if you’re writing sort of
relatively performance-critical code, where you don’t want to, like clone
all
the elements and then call clear
on the map, for example.
There’s another interesting function that some people might, sort of, squint at
and go, why was this even added? Which is the get_key_value
method on
BTreeMap
and HashMap
. And previously there’s always been, sort of, the get
function, which, you give a key, and then you get a reference to the value. But
get_key_value
seems a little odd at first glance, because if I give a key, why
would I want a reference to the key given back to me? And this gets at a pretty
subtle thing, that we’re not gonna spend too much time thinking about, or
talking about. But it is interesting to know about, which is, if you look
carefully at the method signature for the get
function, you will see that it
doesn’t take a key, that is, a reference to K
, the key type of the map. It
takes a reference to Q
. It’s generic over Q
and takes a reference to Q
,
where K
can be borrowed as a Q
. So that is, if you have a K
, you can get a
reference to a Q
, and then the way it internally works, is you give it this
Q
type, and that Q
type has to be comparable with the key type, if you
borrow it.
This might seem relatively involved; I suggest you look at the method signature.
But what this enables you to do is that you can have a key type that stores
additional information beyond whatever the unique identifier for the element is.
This could be something like, it also, stores the length of the string or the
hash code of the key that’s actually there, or something like that. Some
auxiliary information that isn’t really important for the purposes of using it
as a key in the map. And you can still just use the unique identifier to do the
lookup. So, for example, I might have a really complex key type that has all
sorts of fields. But there’s, like, a string in there; that’s really what the
key is. And as long as I can borrow that key type into a string, then I can just
use a string as the key to do the look up in the map, and get_key_value
now
allows me to get back a reference to that more complex key type that’s actually
stored in the map, which I don’t have just to do the look up. It’s a relatively
involved explanation, and it’s a function that you will probably need relatively
rarely. But if you need it, this is the only efficient way to get this
functionality.
Do you want to talk about some of the new methods that Option
has gotten?
Ben: Not really, like, I think it’s kind of just par for the course of
adding in new things. So for example, if you even look at the release notes
where the new functions are Option::as_deref
, Option::as_deref_mut
, and
they’re, kind of, just analogous to the existing Option::as_ref
and
Option::as_mut
. But they allow you to leverage the Deref
and DerefMut
traits. So it’s kind of just making Option
more generally useful and more
generally consistent with things that already exist, and more flexible, I
suppose. So it’s, kind of, just par for the course for— we added in a, kind of,
little convenience method. Sometimes it will be useful. Maybe not.
Do you want to talk about the— perhaps, the bigger thing for me is the floats to bytes functions that are now found in— the for floating point numbers?
Jon: Oh, yeah, this is pretty handy. So, we talked about this on one of the
previous episodes, I believe, which is when these methods were added for
integers. And so this is— imagine that you want to send a number over the wire,
and so you need to serialize it into bytes that you’re actually going to send
over TCP or whatnot. Previously, a bunch of methods on various types— various
integer types were stabilized, that let you take a number and turn it into
bytes, or take a sequence of bytes and turn it into a number, without having to
resort to unsafe code. And now, in 1.40 similar methods exist for the floating
point types. This is just sort of a handy extension of that. And hopefully will
make you reach for the byteorder
crate a little less often, even though it is
a great crate.
I think another one that’s worth mentioning is— sorry go ahead.
Ben: I was going to, kind of, just talk about how— I really like the idea
here, of when you’re adding functions to the standard library, to kind of look
at what is popular out there in the ecosystem. Think about, sort of, what crates
does everyone depend on, transitively or otherwise? How can we, sort of like, is
there any functionality from those crates that it would make sense to have in
std
? That would, sort of, benefit a lot of use cases while also potentially
reducing some dependency graphs. If you just need, say, in this case, the
ability to send some floating point numbers over the wire, then now you can not
use the byteorder
crate, and which, kind of, I know people are kind of— in any
case, you are sensitive to the idea of, in any of your applications written in
Rust, the size of your dependency tree, and so beginning to reduce that, I
think, is personally my theme for Rust 2020. I didn’t get around actually
writing a blog post, but I think there’s plenty of things in the wire, that are
of this sort where, we are looking, examining the most popular crates on
crates.io, figuring out, hey, like, what are some that are, like, small and
self-contained and ubiquitous, and easy to provide for people. And so my current
favorite example is the lazy
crate, or the once_cell
, which provides a lazy
type, which pretty much would just replace lazy_static
for me entirely. And so
it’s a macro-free way of doing what lazy_static
currently does, and there’s
also— I’m not sure if this has landed yet. But there is a crate out there called
matches
, which just provides a macro that tells you whether or not some
pattern will match some value. And someone was doing some digging around, and
just looking at hey, like, what is the single most, like, transitively depended-
on crate on crates.io, and it was this crate. And so I think now, that it is
actually— either already included or being proposed for inclusion in std
in
nightly.
Jon: That PR has actually already landed, and I think it’s even stabilized. So I think we’re getting that in 1.41.
Ben: Nice.
Jon: Yeah. I’ve been following that PR too, cause It’s really cool.
Ben: So it’s a very simple new addition. Doing things that, honestly, I
personally haven’t needed. But apparently, a lot of people do. So it’s good to
see, standard library, tentatively— like, we’re never going to have the world’s
biggest standard library in Rust. Like, I wouldn’t expect to ever see, say,
hyper
in the standard library. But it’s good to see things moving towards very
simple common dependencies being upstreamed into std
.
Jon: Yeah, I agree. It’s a tricky balance, though, because you also don’t
want std
to get too large. And so it really, as you say, the process is pretty
much, look at what does basically everyone depend on, at least transitively, and
then find some common core that, like, if we just had this in the standard
library, that would cover, like, 80% of use cases?
Ben: Yeah, I would love some day to write up a whole treatise on, hey what
are the criteria that we use to gauge suitability for inclusion in std
, where
it’s like, maintenance burden, and like, standardization of a thing, like,
broadly and, you know, precedents from other languages. And, will this thing
change, given future language features? Or, how will the API potentially change?
Is this stable? Is this widely used? There’s a whole lot to consider, and I
think, you know, in the past it’s been extremely conservative, with, okay,
standard library additions will mostly just add new convenience features for
things that already exist, effectively making the standard library deeper but
not broader in the sense of adding in new modules. And so, for example, Python
is a very deep and broad standard library, where something like Go is not as
deep but very broad. And Rust is not as broad, but very deep. So there are
different approaches to these things.
Jon: Yeah, and it’s interesting because when you read through some of the PRs and RFCs that propose these features, very often the discussion ends up circling around like, is this something that is worthwhile to include in the standard library, because there’s sort of a commitment that comes with that, too.
Ben: Yeah. Again, maintenance burden, and you have to maintain it for all time is the question.
Jon: Exactly. That brings us to the end of the, sort of, official release notes. But there is, sort of at the bottom of each of these release notes, there’s this little section called “other changes” that links to the actual changelog in both Rust, Cargo and Clippy. And I always love digging into this, because I find some, like, nice little gems there. And there are two in particular I wanted to highlight this time, that may or may not affect you, but I thought they were interesting.
The first of these is that Cargo will now remember the warnings for your crate if it compiled without errors, and then if you run Cargo again, it will print those warnings again.
Ben: I love that.
Jon: This has been, like, a hill for me for so long where— you might have
noticed this with Clippy. But even just with, like, cargo check
or something,
where you run it, it creates some error— it creates some warnings, but it
compiled just fine. You run the command again, and it just says, like— it just
completes immediately and says nothing. And it’s really annoying. And now that
has been fixed. Now it will just, always display the same warnings, even if the
crate has already been compiled.
The other thing is a change to the way that feature flags work in workspaces. We
haven’t really talked too much about Cargo workspaces, but suffice to say,
they’re a way of having multiple crates under one crate. And previously you
could try to pass the --features
flag inside the root of such a workspace, and
that has now been turned into a hard error. That command will just error out
saying that does not work. And this might come as a piece of— like, this might
be frustrating to some people, because they’re like, ah, but I used to use this
and now I can’t do it anymore. And what the release notes pointed out was that
it actually used to do nothing. So if you used to do this, you were just
compiling your crate twice with the same set of features. Passing this flag did
absolutely nothing. And so that is why it is now a hard error. It is not as
though the functionality was removed. It was a minor point, but it was an
interesting one for those who might find this— might accidentally discover this
change, and then get frustrated.
I think with that, those are all the things for 1.40.
Ben: Yeah, we had a bit more to talk about than we thought.
Jon: Yeah, it’s true. It’s a smaller release than 1.39, but at the same time, there are a lot of really interesting changes that, as we talked about last time, like, there’s been a lot of focus on async and await, and this is a release that has nothing about async/await in it. And yet there’s still a lot of meat to it. And I think that is something that we’re gonna expect to continue to see in the releases to come, that they’re just more and more of these features that are not focused on async/await now that that effort has sort of been unblocked from its major hurdle.
Ben: Well, speaking of continuing, now that we’ve established only releasing one episode every decade, I think now we have to wait until 2030 to do our next episode. So I think it’s gonna be about Rust 128 at that point. So I suppose I’ll see you in 10 years, Jon.
Jon: That’s great. I’m looking forward to it already, Ben.
Ben: I can’t wait to not see you for 10 years. That sounds great. Thanks a lot.
Jon: Exactly.
Ben: All right. Yeah. OK. See you, folks.
Jon: All right. Bye, everyone.