So, hello everyone, I'm happy to share some experience of mine with building Rust libraries.
There's a lot that I went over to come up with like the lessons that I actually feel
like are worth sharing and is much more than can fit into a talk, so I tried to sort of
get it down to the things that are most important to me, but if we have some space at the end
then we can sort of dive deeper into some of the things that I will only cover very briefly.
So who am I?
My name is Amin Roneha and I have been creating open source libraries for quite a while.
I have originally written a lot of Python code, I wrote the Flask Microphone work for
Python and I have been doing open source libraries probably since I'm 17 or something, so this
is many years of experience in some sense and I started using Rust in one form or another
almost exactly 10 years ago, so I have played with this language for really long at this
point and so I went through some iterations of what works and what doesn't work.
Commercially now I work for a company called Sentry, we are doing application monitoring
and crash reporting and so we use Rust there and so I'm going to share the learnings of
this in sort of two ways.
One is open source libraries that are there for other people to use and the other one
is libraries that are to be used within the context of one specific company because some
of the things that we create, the only users really are us ourselves.
And so in terms of open source libraries that sort of feed some into this talk, I'm taking
a little bit of code from my Insta snapshot in testing library, my mini-changer template
engine and a few others like my console library.
Okay, so this is a quote that I read a couple of months ago and it's not so important but
it really bothered me.
It was a comment to an open source library written in Rust that more or less made a
statement that defaults shouldn't exist and people should think really, really hard to
spend the mental capacity of understanding what they should be doing, picking defaults
blindly is a mistake and I don't subscribe to this at all because I believe that it's
the responsibility of a library author to make sure that the defaults are the best that
you can possibly take and the way I would think of this is you can think of creating
a library or an API as if you're building a business.
You have some sort of success criteria and you can measure yourself against this success
criteria and I don't think the success criteria should ever be to confuse your user as much
as possible.
It should be that you measure how successful your customers are in using the API as you
designed it, so if they do something that is not ideal, there's something you can fix
and the second thing is then they will typically use your API to build a product themselves,
to solve a problem themselves and the quality of the output that the user then produces
in some way is also something that you have an impact on.
So you can kind of think of it like the percentage of users that are making the wrong or right
choices is the dial that you can sort of try to dial.
The problem obviously is how the hell do you measure this?
And it's really, really hard because most of the things that you can actually measure,
they don't really show up in useful things, so you can define the success criteria and
I think a lot of people do this in some way subconsciously, but they absolutely have no
way to measure it.
I don't have a way to measure most of those things.
So the only thing you can do in the beginning is measure yourself, so you treat yourself
as the first user, horrible sample size, but you can sort of figure out like as you are
using your own stuff, does it do what you think it does?
And then every time you sort of hate the experience, at least write down like it didn't like this.
So it's a good start, but the problem is you're really flying blind, there's no good way to
measure this and this is not unique to Rust, this is the fact most of the time it works
like this.
But in terms of API design, I think we have learned in other environments that there's
actually a lot of stuff to measure, so if you were to create an HTTP library, a lot
of companies are trying to figure out how often do the users that hit the APIs do the
things that they don't want them to do, for instance, because they create bad load patterns
or because they just generally hit the API in ways that is not efficient and they're
trying to figure out like how do we optimize so that the user actually does the thing that
you want.
And this works if you run a service because you can see what everybody is doing.
You can't really see anything that the developer is doing in your Rust library.
The only numbers that we have for download statistics which are really pointless because
they're heavily skewed towards libraries that are used by other libraries, so you can build
the most amazing library ever to build an application, but if there's only ever application
developers that are going to download this library, you're never going to accumulate
a lot of download statistics because most of the download statistics come from either
CI and very few people actually download the code to run on a desktop.
So if you have 100 developers, they're going to pull this once each usually and then it's
cached on the machine.
The download numbers come from CI, so it's only this dependencies of dependencies that
are actually ending up driving those numbers, and so they can be demotivating in some sense
and they're definitely not that useful, so you can't really track anything sensible in
terms of adoption just by the download numbers.
So the first feedback you're probably going to get is some form of frustration, so it's
usually bug reports or it's someone internally telling you that this thing is really inconvenient
to use.
In some ways, you have to prompt them to actually tell you that, but you can kind of do it like
use a service in interviews to figure out like, do people like this, and you kind of
usually don't get people to reply to service, so one other way to sort of solicit feedback
out of people is you take this issue request, ignore it entirely almost, and you go back
to why did they submit this in the first place, and they try to ask that question, because
quite often when they submit a bug report, they're already down a really weird path anyways.
Maybe you can take them on a higher level and figure out why did they even try to do
this.
I mean, when they do a feature request, bug report is less so.
So as mentioned, it's really hard to measure, and so because it's really hard to measure,
a second thing that you can sort of use is trying to figure out like, okay, if you think
this is worth measuring, what is it that this number actually represents?
This is typically some sort of values, and so these are the ones that I find are important
to me.
They're probably not necessarily important for everybody else, but I felt like over
the years I can get behind those.
So the first one is I think the hello world of your library should be relatively concise,
so it should be easy enough to get started, to some degree this is simplifying onboarding,
but to another one it's also that the less stuff there is, the easier it is to maintain
the whole thing overall, or gotten to this a little bit.
This also leads towards good defaults, ideally you don't have to copy paste five lines of
code in if a single line of code does.
The smaller the surface area of a library actually is at the end of the day, the easier
you have in terms of maintenance and also in the ability to do modifications over time.
Because the more API you expose, the higher the chance that you're going to break something,
and I don't like breaking things.
I think backwards compatibility is really important.
I hate the idea of unnecessary churn.
I've been part of the Python 2 to Python 3 migration thing, and it was horrible, and
it was particularly horrible because a lot of people that really liked the language,
as I've included, got stuck on Python 2 for a really long time because we used it in a
commercial context, we used it for large projects, and those were the ones that moved the slowest.
You had a lot of power users stuck on an old version that actually used it in a quite
extensive context, and it took us a couple of years to move, and while we couldn't move
or we could only play with it in small experiments, the language kept innovating, and all those
innovations were unable to be used by the people that previously were really engaged
in their thing.
I don't like the idea of letting people behind, so the easier the migration path is forward,
the fewer people are going to leave behind on all the versions.
In a way, my goal is to keep people on the golden path.
What's the golden path?
It's the idea that you have an idea of how people should be building stuff.
Typically, this is a thing that you do in system design, and you plot an idea for someone
to execute along, and this is the blessed way to doing things, and you try to get as
many people as possible on this golden path, but libraries have the same thing.
If you want to build a security library, for instance, like an encryption standard library,
there are many ways to build the wrong one, where you give people so many choices that
they have no sensible way to being sure that they are on the one that actually does the
right thing.
But the problem with the golden path is that it will change over time.
What is correct today might not necessarily be correct in two years.
To go back to the security thing, at one point, we recommended everybody MD5, and then that
was not a good idea anymore, so at the very least, it should be SHA-1 hashes, and now obviously
we don't want that anymore.
The problem with this is that a lot of things change over time, and so if you create, for
instance, a library that wants to make a choice for a user, but it's designed in a way that
that choice can never be changed on the fear of breaking code to some degree, and then
you did something wrong in the sign origin already.
Some change requires adjustment by users, and that's really hard to do, so if you have
some sort of golden path, you can figure out how many people are on that, versus the thing
that you don't want them to do anymore, and again, measuring is almost impossible, but
one way you can do that is if you are building a library where you think other libraries are
going to use that, you can use something like a GitHub code search to figure out how many
people still use the deprecated pattern.
You can also leverage dependabot quite a bit to figure out how many people that actually
are getting a dependabot update are going through with the update versus not.
Okay, so defaults matter.
As mentioned earlier, I'm a strong believer in there should be good defaults, and I think
defaults really come in two choices.
One default is the absolute default.
You're never going to change the default of a U32 integer to be anything other than zero
and rust.
There wouldn't even be a discussion about it to change it to one, right?
There's an obvious default, and it's so dependent on that you just will never be able to change
it.
But then there are sort of defaults that are intentionally designed so that they can change
over time.
For me, a good example here is the hash table in Rust or any hashing function in Rust doesn't
say what hashing algorithm uses.
It defines some sort of properties around it within the boundaries of which it probably
doesn't change, but the Rust developers could come in from one day to another and switch
from SIP hash to FX hash, and most of us wouldn't notice, right?
So to enable these sort of defaults, you have to design the system around it so that these
can be changed, and you have to communicate to user how you're going to change them.
There have to be some rules and expectations around what the stability here means, but
you should try to aim that you can actually enable this change, because otherwise you end
up in a situation where you have created an API and now you need to make a second one
because the old one just doesn't work anymore, and you either have to take it away, it's
going to be frustrating for user, or you have to lie about what it does.
There are a lot of liars out there that say, hey, my function does SIP hash, and then in
reality it just doesn't do that anymore because they wanted people to move up, but they were
afraid of breaking it.
So why does this work in Rust, for instance, with the hasher?
Because they said, okay, this is never going to be portable, and I think this is a really
interesting part because it also enforces without saying what it is, how good it is.
What I mean with this is that the hasher in Rust randomizes all the time.
If you even try to use it in a portable way, you're going to quickly figure out that every
time you re-run your program, the hash is different, so you cannot even get into this
mood of trying to use it for something that's portable, and there's a really good analog
of something like this, which is every once in a while people build programming languages,
and I think Go went down this very same path where they said, we're going to build a garbage
collector, and it doesn't compact right away, but it's eventually going to be a compacting
garbage collector.
And the problem is if you don't start out with actually writing a compacting garbage
collector, a compacting garbage collector takes a pointer and moves it somewhere else,
so to compress down the heap space.
But if you never compact to begin with, people get used to this idea that the point is really
stable, and they will stash the pointer somewhere, and they rely on the fact that it never moves.
And so at a later point, three years in, someone says, oh, now we built this awesome
compacting garbage collector, but they can never turn it on because people took advantage
of the fact that there was actually a conveyance in the API that they used that wasn't supposed
to be there, but it wasn't enforced.
And so what the Rust hasher does in this sense is it always randomizes, so it already makes
it so uncomfortable to use for the wrong thing that you wouldn't use it.
And so if you were, for instance, to build another language with a desire to eventually
build a compacting garbage collector into it, you would probably try to make it so uncomfortable
to use these pointers for stashing away that it already wouldn't work in the absence of
one.
So I think it's a really important part to make sure that if you want to be able to create
this, change the defaults, that it start from the beginning thinking, OK, how do I build
it so that I actually remain, keep this freedom.
And so why do I even want to change this?
Well, because you have this problem of cargo-culting.
I have created things in the past myself where I discovered that people copy-paste the first
example from documentation over and over and over again.
And then you have this first example from documentation in millions of repositories.
And for me, the weirdest one is that one of my frameworks that I built actually ended
up in a university course as a programming 101.
And universities are sort of an amazing catalyst for this because you end up with all of those
students putting their GitHub repositories of the first courses on GitHub.
And then you see the madness you have created, multiplied through like every single student
that on boards.
And it's a real lesson.
So I really respond really negatively to this idea of this code example, which I know it's
hard to read, but it basically is sort of the, that would be the equivalent of not picking
a default hash.
It would be like, well, we think that Rust is a hard programming language.
It's supposed to be like C++, you should think.
So you pick the hasher yourself, and then you just put it in there.
And then I swear to you, this would be the first code example, even if the documentation
says you should pick FX hasher for this type of hash map, and you should use SIP hasher
for this other hash map.
Nobody would read it.
They take the first code example and copy paste it in, right?
But now we discover maybe SIP hasher actually is better than FX hasher, but we cannot change
everybody's code all at once because everybody has copy pasted the wrong thing over there.
And what I know happens is that every once in a while, someone comes and is like, okay,
we know that FX hasher is really popular, so we're going behind the scenes, change it
to actually be a different hasher because everybody copy pasted this thing everywhere.
It's incredibly common that you find an API that says, like, I'm named really misleadingly
because this used to be my implementation, but now I'm something else entirely, right?
So it doesn't, like this card-culting I hate, and I don't think it's good, but I understand
it's really appealing because it's flexibility.
This I think, I actually made the wrong code example here on the slide, but I was too lazy
to change it afterwards, but just imagine for a second that this code example wouldn't
return a string, but actually returns an enum with a variant called char256, but the idea
here is more or less this.
If you do create a library that does something of which in the future there might be different
implementation coming along, what you probably want to do is not just return the output,
but you want to annotate the output with what kind of thing it is.
So if you create a, if you have a library that sort of is supposed to validate files
on the file system, and it does it by calculating a checksum, it's much better for that library
not to return the bytes of the checksum, but to return the bytes of the checksum and the
information by the way I'm char256.
Why?
Mostly because it forces the developer to recognize that this thing might change, right?
It might not be char256 forever.
And this sort of explicit, like even if you never plan on actually changing this at all,
it still forces something in the mind of the developer to recognize that this is probably
going to change.
And I know of a version control system that very famously picked one shahesh and is completely
incapable of changing it now, not because it's technically impossible, they've changed
everything, but because there's an ecosystem of stuff that assumed that it will never change,
this change cannot be pushed through, right?
They have the one version, the other version, the incompatible of each other as an example
of this, right?
I know this is very security heavy right now, but the idea is the same, like you, every
once in a while there's a reason for you to move up to a different algorithm or something
of that nature, and if you haven't created an API in the beginning that sort of hammers
down this point that it might change, people will not necessarily think about this.
Less is more, I think it's pretty obvious, the larger the API surface, the more stuff
you will have to make sure doesn't break if you change internals.
Particularly internal APIs are incredibly leaky accidentally or intentionally, and whenever
you want to change it, you have to ask yourself the question if someone actually using this.
A lot of people start out building libraries where every single module, every single function
is just pop, and every version breaks everything ever, because you just don't have the time
and, at least you don't have the time usually, and the capabilities to figure out if someone
actually uses all of those functions, so you just break them all.
Or you just spend all this extra time on making sure that they never break because it's impossible.
So goal is small API surface, but then if you go to a small API surface, then there's
some tricks in Rust you can use to reduce API surface, but not really.
So for instance, we have some really common abstractions like Intot or SRFT, which can
be used to be generic over something.
So it saves you the necessity to build a function four times.
You might want to open a file by string or you might want to open a file by path.
Just write one function, you can do SRFT path, and it's possible to pass at any of those
compatible types.
Now, it's kind of cheating because it's really still four different APIs, but they're hidden
behind a standard constructor that the developer understands.
So this is an example from Minigenger.
Both name and value are generic.
One is converting into a copy and write string thing.
So you can either pass it an own string or you can pass a static string reference.
Both of those will be fine.
So from an API point of view, it behaves as you would expect.
The first thing is a string argument, like you cannot get it wrong.
And the second thing is whatever converts into my internal value type of which there
are a bunch of conversions.
I could also have made seven different at whatever functions for every single data type
that exists, but this is a simpler way to do it.
And I could also have said, like, hey, you're going to pass the value directly already,
but since I already need to provide a way for the user anyways to convert it into this
value type, might as well use this.
So it's very consistently throughout the API now using int of value.
So there's only one function and it hides all of those different variations of actual
API that still exists behind this int of thing.
SRF is sort of the same.
I actually hate SRF because it makes it really unreadable.
So instead of your function taking a path by reference, it takes a p and then the p is
hidden behind SRF path.
And then before to use it, very often I use SRF.toPathPath, which is just weird, but this
is a really common pattern.
So I don't like it, but it's so standardized in the ecosystem that people get to expect
it.
And so because we all have been willing to live with it, I guess this is sort of enough
of a reason to keep doing it.
If you have this sort of APIs that use a lot of generics to do type conversions, you will
immediately blow up your compile times.
This is the standard trick to not do that, which is you keep the generic part into a
tiny, tiny function and then call it the non-generic part.
So in this case, the conversion goes to this value type.
I know it's really hard to see in this projector, but basically the function render takes an
object that can serialize with 30, and then it calls value from serializable, which takes
this value, converts it into a value type, which is the non-generic, which I use all
over the thing.
And then all the function underscore, render, underscore, eval, they will not be generic.
And so they have one instance, whereas the other one will be one instance per type actually
passed.
What does it mean?
If you don't do this, if this was all one function, last time I checked this, the mini-changer
compile times are exploding to more than double or three times just because of this one function.
Why?
Because it's very common that you pass different types of it, and the Rust compiler thinks that
inlining is awesome, and it inlines this VM eval function there, which is, I think,
like 15 kilobytes or something, and so it makes a duplication of those 16 kilobytes for
every single type that you put in.
In this case, I only have 16 kilobytes once, right?
So it's a serious amount of code that you save with this, and it's probably the best
way to save compile times for a lot of code.
There are actually some tools that you can use these days to figure out, like, why these
functions blow up that much.
But if you use generics, particularly with SRF and into, it's worth looking for.
So I mentioned earlier, I like to keep the API service as small as possible.
That doesn't mean that I don't build actually an onion of API behind the scenes.
So I like this onion concept, like, APIs are layered.
But the user only gets the outermost layer, the inner layers are for own enjoyment.
And then you keep these inner layers, and you have this flexibility of change remaining,
but it's still kind of clean.
And then over time, if that library becomes like the most stable thing ever, you can actually
start exposing the next layers of the onion.
A good example for this, I think, is Rust compiler plugins.
Rust compiler, like many other systems, want you to be able to write a custom plugin for
it, like the proc macros.
But the problem is that that exposes a lot of the internal machinery of the compiler,
which the compiler authors don't want to be stable.
So what they did is they actually took the internals of that thing, they exposed it through
a secondary library, which I think was soon, and I think soon is the one on top, but maybe
it was proc macros too, but in any case, it was exposed as a secondary library.
It actually used the same code, but they had different stability guarantees, and then
they used a conversion system, which was basically serialized to string and deserialized from
string to bridge between the two.
And so if you do feel like, okay, I actually want to eventually expose this other library,
and I want this inner most functionality to be available for customer, you can think
of exposing it as a completely independent library for experimentation, and then eventually
when it stabilizes, you can give it another onion layer to the customer.
But again, once you have exposed that, your stability guarantees are going to be much
harder to uphold.
So just a simple example, internally in my templating engine, I have a lot of abstractions
like compile template, which is something that I know would be awesome to use externally,
but I don't want to give it to you, and then the code generator and the parser that have
already been people asking how to get access to it, you're not going to get it for now.
But I keep the abstraction internally because it's much more fun this way.
As I mentioned, I like to have as little API service as possible, and by the way, I fail
at this.
You will notice that when I start a library, usually it has way more API than after three
iterations when I take it away.
But I basically try to keep it flat, so I expose all of the things I want you to use
to the top level, and then all of my internal create structure is whatever, however I feel
like to actually lay out those things.
If I do have so much code in the library that I feel like there need to be sub-modules,
make them up on the spot, so in the Insta Snapshot testing library that I wrote, I have a lot
of internal types that you actually want to use as a user every once in a while, and they
come from all different locations in the code base.
So what I did is in my librs, I have a pop-mod internals, which is documented, those are
internals you're allowed to use, but I just pull them from wherever they're defined.
So this module doesn't exist as an internals.rs file.
It's purely a thing that sits in my librs to organize the structure.
And that way, I have the freedom to move these things around as I want.
Related, every once in a while, you have to export an API that has to be public for reasons
that mostly sit in the restrictions of the language.
But I really don't want it to use it, so I just hide it.
So dock hidden, you will find a lot of Rust libraries which have a lot more public API,
but you're not supposed to use it.
It's usually hidden behind underscore underscore, and it's just hidden.
Why does it exist?
On the one hand, because Microsoft needs to access functionality.
The other reason is also that you might have to create dependencies.
So Insta, for instance, has to expose internals out of itself into the test runner called
Cargo Insta.
And the way I do this, I have a hidden feature flag which says if you're Cargo Insta, you
can use this feature flag.
Everybody else, just please don't.
But if you do still use that feature flag, you're still not going to see all of this
API because it's kind of hidden away.
This also helps when you use, for instance, DocsRS is an awesome tool to document your
crates.
A lot of people run DocsRS with all feature flags so that it's fully documented everything.
If I wouldn't hide all of those things, then this internal feature flag that would turn
on this API would accidentally document it.
I don't want that.
So even the APIs that sort of are there for me, I will still hide from documentation.
Cool, traits.
I have a really complicated relationship with them because I think they're awesome, but
I also kind of don't want them to be in APIs.
I think they fall into two categories.
Some of them are, well, maybe the other way around, open traits are most common one.
Asref into, everybody can implement them.
And then there are a lot of cases where you might have traits that only exist as an internal
abstraction.
Let's call them sealed traits.
They come from a crate and they're only implemented in the crate and nobody ever should implement
those.
Why do they exist?
Because, for instance, you want to box something up or because you want to have really nice
documentation where you can select, my crate accepts these types of things.
So I wrote a Rust Redis library a couple of years ago where I made the conversion trait
so that you can pass any Rust type that is compatible with Redis in all of those functions.
And I didn't seal it off.
And now it's public interface and people have been sealing off their own types, have been
implementing for their own types.
And so now it's just there.
You can't take it away anymore.
Which is fine for that library, but I didn't want to do it in other cases because, to me,
that trait actually mostly only existed so that it's there for documentation purposes.
So you can seal them in two ways.
One is sort of the soft seal, which is just hide the functions that you're supposed to
implement.
And then it's unclear how you're actually going to implement it.
In this case, it's soft sealed, so you can actually call it still and you can actually
implement it.
I'm not sure if I like it or not, but there are actually some use cases of why you might
want to do this.
Mostly you have to do it with code generation.
But there's also the way of doing a full seal where you put a private serial size type
into an argument and because it cannot name the type because it's never exposed, you can
absolutely not call this ever.
And the funny thing is Rust actually depends in the standard library of this pattern a lot.
If you use any type and the generated downcasting feature, it uses an internal system called
type ID to return a unique number for a type.
If you were to be able to override this and lie about your own type, you could cause unsafe
memory access, right?
And so they have a seal marker in there that you can't implement and only the default implementation
is only the valid only implementation.
So it's a pattern.
It's just not a pattern that has syntax support.
It's just used that way.
For now, maybe it will eventually get one.
So why do I not like traits that much?
Because I find them incredibly hard to discover.
It's very annoying if you import a type from your library and then you want to call a method
on it and the method is not there until you bring the traits into scope.
And I know that a lot of people write this awesome preludes where like import this from
my library and now, by the way, you have 20 types sitting around in your scope.
It works, but it has a whole bunch of problems.
One of which is that it makes your error messages a lot more confusing because there are traits
in a standard library that if they are in scope, types with the inherent implementations
will no longer function the same.
There's a type called, I think it's called borough, a trait called borough.
If you bring borough into your scope, you can no longer borrow from, I think, a ref cell.
It will error because the trait overwrites the inherent implementation.
So that's not great and there's a bug report from like seven years ago, I think, and there's
already, but how do you change it now?
It's like that's the behavior, but that's the problem that you have with preludes because
they are just based by name and it will say like, well, there is no implementation by
that name or it doesn't help you that there's an inherent method on this thing.
I'm not going to let you call it.
You have to disambigate your call.
So there is some problem coming with the trait stuff and they're really useful, but you shouldn't
maybe overdo them, I think, to some degree.
It's really nice if the type out of the box already has a neat little behavior.
And if you want to abstract to a multiple stuff, you can add the traits later anyways.
There are however traits you should probably implement all the time.
My favorite one is debug.
It should be on all your public types.
If you feel like it's blowing up your code generation times too much, you can hide it
behind an external and extra flag you can turn on demand.
I give you a very good reason why debug should be on all your types.
Every once in a while, you have to be generic over something and then you will usually say
like, I'm generic over serialize or I'm generic over my really cool thing that you should
be calling.
And the problem is when you're generic over serialize only and you're deep down in nested
generic over serialize type and you want to debug print, you can no longer do that because
serialize does not imply debug.
So even if a type is capable of debug serializing because it's not a trade bound on the thing,
you cannot call it there, which is very, very annoying in a lot of code setups.
And so what I do these days, I will make debug and implied bound on a lot of, so like a super
trade on a lot of the things that I actually expect to be very deep in code calls so that
I can actually debug the whole thing.
And the problem is that that requires that the ecosystem embraces the idea that almost
everything is debuggable.
So just make it debug.
And if you really think it's blowing up a compile time, just put a second flag in that
it can turn on when you need it.
But not being able to debug very deep down in generic code land is just an unnecessary
frustration you can avoid.
This trade is almost like debug, it just converts this thing to string.
It's obviously useful for a bunch of cases, but I can definitely overdo it.
I much rather have the compiler yell at me that I should be telling it how to format
rather than the string, that the type in itself just being formatable.
But there are obviously like a lot of types for which it's necessary.
Like if you have a UIA IDE, you would expect that the stringifies.
If you have an error, errors have to be stringified so you can't really opt out of it anyways.
But I feel like that a lot of types, actual way to convert it into a string is the debug
thing is really hard to discover API.
Like yes, of course, then I get two string methods appears, but you don't know if this
is a thing that you're supposed to look at or if it's just a debug format with a different
output.
I've seen both.
There are some types where you can call two string on it, but it's really not the real
string form, you have to pick a different one, and it's just an alias for debug print,
and sometimes it's the real deal.
So this is a very confusing trait, I think.
Copy and clone are obviously the most common traits that you have to deal with.
Once granted, impossible to take away, I would say, because people start expecting that they
can clone it.
And so copy is a really tricky one because you can only uphold it for as long as it looks
like a certain type of structure.
Once you put something in there that's not copied, there is no way to fake the copy.
And so if you make your type copy and then you later discover that that was a mistake,
you now need to hold an arc, it's a breaking change, and I don't like breaking changes.
So about copy, you have to think, will you uphold this forever?
Clone, I think, is actually relatively fine to backport.
If you discover over time that you really can't clone the thing anymore, you just put
an arc over it.
So a very common reason in the past, I've noticed that I have a type, it has cloned,
and now it can no longer be cloned because it holds a boxed-in function.
Doesn't actually matter.
Instead of a boxed-in function, make an arced-in function, and it works again.
So a lot of these sort of classic cases of, hey, I can't clone this anymore because of
a reason, you just arc it, it's fine.
And then the problem with copy is also that the inverse, you don't put copy on something
and then people get really angry because they expect it to copy.
Classic case is the range type in Rust.
It doesn't copy, everybody is like, it doesn't understand why does it not copy.
There's a good reason why it doesn't copy, but it's really annoying, and I think it's
one of the most common frustrations in Rust that the range type is not copy, and that you
have to clone it all the time, like this by response on GitHub, I think.
This is one of our only briefly cover, but I don't have recommendations on if something
should be sent or synced, but I do think that it's actually totally fine not to have objects
that are thread safe.
And the reason for that is that you can often just tell a user, they can create an API where
the user doesn't really have to do this.
They can just put this locally and they're done.
Why?
Because, for instance, one of the things that I love to do is create this sort of session
abstraction.
The session abstraction is I want to deal with a bunch of objects for the scope of one
function only.
So I load everything into some sort of environment where I do some stuff with it, I have an object
called the session, it tracks a whole bunch of data modifications, it gives me access
to something, it's the only thing that ever needs to hold this thing, and then when I'm
done with it, I tear down the session and my lifetime problem goes away.
So for as long as you don't have to pass this over really complicated boundaries, just
have this thing, make a session abstraction, hold all the data in there that borrows temporarily
and then disregard it.
So this is from our Symbolication Library, it holds, when you want to deal with a debug
file, you open up this debug session, you do a bunch of stuff, you tear it down.
And we have never, I think, had a case where we need to hold on to this, where the lifetime
was actually a problem.
But you will notice here there's a self-cell in there, which I think is probably one of
the best things you can have.
Then you create an API, and then you discover I need to borrow into myself, and Russ doesn't
do that, but self-cell lets you do that.
So there are a lot of crates that have been created over the years that you can borrow
into yourself, this is my favorite.
It basically gives you almost like, it gives you a type that says here I hold some data
and by the way I also hold a reference into myself, and it doesn't really convenient way.
So if you have created yourself into a whole, where you feel like lifetimes are your enemy,
this might dig you out of this.
Yeah, last part, errors, I think they're important, they're talked by themselves.
You have to consider if you panic or error.
My strong recommendation is not panic, but if you do panic, use track caller, you probably
know what this is, but it basically removes this one function from the cost of the call
stack, and it removes this one function from the call stack, and then the panic message
says, hey, your problem was where you called, in this case, pop on an empty stack, and not
the completely pointless unwrap in there, because you want to know where you're fucked
up and not here, obviously.
Errors are really important, again, they would be a talk by themselves, but my ask to everybody
is please implement STD error on those thingies, because then you can sort of compose them,
a lot of people still don't do, it's very, very annoying if they don't.
Thank you.
So otherwise, we can also do it in the hallway, but might have one or two questions.
Thank you very much for the talk.