WEBVTT

00:00.000 --> 00:10.240
Perfect. So we're going to welcome back Nikolay Vasquez who's going to talk about some

00:10.240 --> 00:14.920
type tips and tricks. Hopefully we all pick something you'll use for what? Thank you very

00:14.920 --> 00:15.920
much.

00:15.920 --> 00:25.560
All right, yeah thanks for having me, this is. Happy to be back at Boston, my second

00:25.560 --> 00:33.680
time speaking, so I'm looking forward to it. So the topic today is about using Rust type

00:33.680 --> 00:40.240
system in advanced ways that will assume a decent knowledge of Rust beforehand, not going

00:40.240 --> 00:48.080
to be covering many basics. So if you get lost, it's non-tyrely or fault, but otherwise

00:48.080 --> 00:57.320
you can look up references for what I cover. So before I get started, what of the motivations

00:57.320 --> 01:01.080
for this talk is that a lot of the techniques I use are in my benchmarking library

01:01.080 --> 01:10.040
Devon, a show of hands who here is sort of Devon? Okay, nice, so decent amount. So this

01:10.040 --> 01:17.320
was my talk at Boston last year was introducing Devon and so it takes advantage of the type

01:17.400 --> 01:24.960
system in very interesting ways as you'll see. And before I get started, I'm hoping to

01:24.960 --> 01:30.960
make a full-time job and build a team around it, so I'm looking for sponsorship to help

01:30.960 --> 01:36.840
make that a reality. So please check out the project and send money towards if you can.

01:36.840 --> 01:39.120
Thank you.

01:39.120 --> 01:43.360
So the topic that I'm going to be covering are types that it's conditional typing, polymorphic

01:43.360 --> 01:50.280
callbacks, and type a ratio. Type states are an extension of the builder pattern, so you

01:50.280 --> 01:56.160
might be familiar with this in Rust where rather than calling a function that takes all

01:56.160 --> 02:02.280
the parameters you can instead provide individual options where some of them are always

02:02.280 --> 02:07.400
necessary. However, one nice thing about the type state pattern is that you can make some

02:07.400 --> 02:13.280
options actually be necessary. So for example, if we remove the name method call here,

02:13.320 --> 02:20.320
we can actually get a compiler saying that the default name is not convertible to a string.

02:20.320 --> 02:24.960
And so if the user doesn't provide a name here for the config, then you get a compiler

02:24.960 --> 02:27.680
instead of a runtime error.

02:27.680 --> 02:35.320
And the way this works is you have to set a default generic state where we know that this

02:35.320 --> 02:41.800
is not going to implement into string. And so as a result, when we later end up calling

02:41.800 --> 02:46.800
a name, we know that, okay, well, the string method, sorry, the name method needed

02:46.800 --> 02:51.800
to be provided at some point. And so the way the sense of working is essentially, you

02:51.800 --> 02:58.000
have a state transition from a default name on set state to then providing some generic

02:58.000 --> 03:04.520
and name, and then that wouldn't be checks until you call build. You could also provide

03:04.520 --> 03:09.880
the name into string at the name call site itself as well, but in the end, this will end

03:09.880 --> 03:19.680
up getting checked. So this technique is not just good for configuration with data, but

03:19.680 --> 03:24.760
you can also use it to configure how execution works. And so in Devon, there's this

03:24.760 --> 03:30.760
venture that you can take optionally, if you want to provide context to your benchmarks.

03:30.760 --> 03:39.560
And then you pass it closer by default to iteratively, keep calling the function and measure

03:39.560 --> 03:46.400
its performance. However, what you can also do instead is you can pass another closure

03:46.400 --> 03:51.960
to generate inputs for every single iteration. And so what this now does is it makes another

03:51.960 --> 04:00.480
method available called bench values, as well as it makes other, other intermediate

04:00.480 --> 04:06.520
builder methods also available. So for example, if you wanted to count throughput of bytes

04:06.600 --> 04:13.400
from your given inputs, this can be provided as a statically check string rather than just

04:13.400 --> 04:20.040
something that gets checked dynamically at runtime. So if we were to remove the width inputs,

04:20.040 --> 04:29.320
we would see that when we call bench values, it's saying that there's unsatisfied

04:29.320 --> 04:36.840
trade balance. And the way this works is that the venture type will take a venture

04:36.840 --> 04:43.240
config by default. And then that is itself a public and private type, which essentially means

04:43.240 --> 04:47.880
that it is, well, it's technically public and never gets actually exported by the crate. So

04:47.880 --> 04:53.640
it never gets possible to use outside of the crate. And so we can wrap all of our generic

04:53.640 --> 04:58.920
parameters inside of this venture config. Here we only just have one which is generate input.

04:59.160 --> 05:06.520
And so as a default, it's essentially like the unit type where you have an empty

05:06.520 --> 05:15.640
tuple, but instead it's an unnameable public and private type. And this default itself is not a

05:15.640 --> 05:22.520
function. And so when you try to call the bench method, as if you had bench values, as if you

05:22.520 --> 05:28.280
had called a provider function, then that won't work at compile time. And we can see how this

05:28.280 --> 05:36.120
gets enforced later in the impulse where the bench method just is implemented on the default

05:36.120 --> 05:43.720
venture type with the default venture config. And then we have a bench values method that will

05:43.720 --> 05:52.760
take the generic parameter that was provided before and then actually invoke it to perform the

05:52.840 --> 05:59.000
bench working. So types of things give us a lot of benefits. They allow us to move

06:00.200 --> 06:06.040
checking your inputs to compile time. And this ends up having runs on performance improvements

06:06.040 --> 06:20.440
in that you only end up paying for fields that you end up setting as well as if. So rather than another

06:20.440 --> 06:25.880
run time improvement, is that rather than be checking at run time whether your inputs are valid,

06:25.880 --> 06:30.440
you instead just get the compile errors. And that ends up being also just a lot

06:30.440 --> 06:36.680
clearer for users as to how they're using your library incorrectly. Another topic I'd like to

06:36.680 --> 06:42.520
cover is conditional typing, or what I like to call dependent types at home. So say you have a

06:42.520 --> 06:49.800
function to log the throughput of bytes that you're processing. And you may consider that that

06:49.800 --> 06:56.920
should take use size because lengths for slices use use size. And for context use size is essentially

06:56.920 --> 07:02.280
just an unsigned integer that's the same size as a pointer. So it can be the length of the whole

07:02.280 --> 07:10.680
address space. And if we had this as use size and we wanted it to work with not just slices but we

07:10.680 --> 07:16.600
also wanted it to work with files, well file lengths require use 64. And so now we have this

07:16.760 --> 07:23.560
type mismatch. And if we ended up trying to use this for slices then we end up saying, okay well

07:23.560 --> 07:29.800
sure we can pass it to use 64. But say for example, let's just say use size ended up being coming

07:29.800 --> 07:38.440
larger than use 64 in a hypothetical 128 bit platform that rust with support. And you could try to

07:38.440 --> 07:44.680
support both of these by wrapping them in an enum. And one of the main downsides of this is now you

07:44.760 --> 07:50.520
have a whole extra word just to determine what is the use size of 64 just to determine the size.

07:50.520 --> 07:57.720
And so there is a bit of a performance penalty here. You could also consider, well let's just use

07:57.720 --> 08:05.240
unions instead, however now you end up reaching for unsafe. So the very creative alternative to this

08:05.240 --> 08:11.560
is you can end up making a type alias to either use size or use 64. And the way that you end up doing

08:11.640 --> 08:20.680
that is using the trade system. So you could parameterize a trade based off of a type or a constant

08:20.680 --> 08:27.880
condition. In this case what I'm doing is I'm making this use size 64 associated type

08:28.840 --> 08:35.880
be available for the empty tuple type just as a placeholder. And we make it available for the condition

08:36.200 --> 08:43.080
of is pointer greater than use 64 true. We make it be use size otherwise if use size as smaller than

08:43.080 --> 08:51.960
use 64 then we want this associated type to be use 64. And if we end up using this we can use this

08:51.960 --> 09:00.680
lovely syntax of okay empty tuple as proxy use 64 and access the associated type. But we can see

09:00.760 --> 09:06.920
that we're actually using use size or use 64 we can assign it to an integer directly and it's not

09:06.920 --> 09:14.280
just like wrapping it with an enum or a union. And so we're using the type directly itself.

09:15.160 --> 09:21.800
I published a great called content that provides a generic type alias for this technique and so

09:21.800 --> 09:27.560
you could also define use size 64 in terms of this type alias. And we see here that you can just

09:27.560 --> 09:33.720
use it for example to assign it to an integer. And if you're going to use ends up taking different

09:33.720 --> 09:41.720
types as the input for the callback. So in this next version of Devon we'll have run time benchmark

09:41.720 --> 09:47.960
registration and to make this API very flexible. It could either take a Devon venture which

09:49.160 --> 09:53.480
has a subtle difference from Devon bench that I'm not going to get into but it'll be clear

09:53.720 --> 10:01.960
in the API eventually. And sorry it means right bunch of twice there but we see that we are able to

10:01.960 --> 10:07.800
invoke the same exact API but we're able to provide it with three different types of closures. One

10:08.920 --> 10:12.680
two of them that take parameters that are different parameters and then one that doesn't take any

10:12.680 --> 10:19.240
parameters. And the way that this flexibility is achieved is with a separate trait where the

10:20.200 --> 10:28.360
key feature here is that we're providing this marker type that essentially allows us to

10:28.920 --> 10:34.440
generate an implement this trait generically without having conflicting implementations.

10:35.160 --> 10:40.040
So in the actual implementation we see that we end up passing as the marker type

10:40.840 --> 10:47.640
fn of bencher, fn of bencher, and then empty fn. And this is just to essentially implement

10:49.080 --> 10:54.360
kind of a different trait for every instance. And we know that we're not going to get a closure

10:54.360 --> 11:02.920
that is going to implement both fn, fn, and fn bencher. And so we don't end up having any

11:03.880 --> 11:10.680
ambiguity errors when we end up trying to use this. And Devon's not the only library that uses

11:10.680 --> 11:18.360
this technique. This technique is also utilized by Bevy's ECS engine where you can handle

11:18.360 --> 11:26.040
quite a flexible number of parameters as dependency injection. And the way that this ends up working

11:26.040 --> 11:31.640
in practice, or at least the way it's implemented rather, is using the same underlying technique

11:31.640 --> 11:39.000
where we have this generic trait for all of the functions that we're going to run against.

11:39.000 --> 11:46.760
And it takes this marker parameter to essentially indicate to the compiler that, okay, these are

11:46.760 --> 11:53.560
separate implementations, and you don't end up with the error of multiple trait,

11:53.560 --> 11:58.360
implementing the same trait across multiple generic types, which is the limitation of the

11:58.360 --> 12:03.160
compiler right now until we end up getting specialization, which may never land, we'll see.

12:04.520 --> 12:10.520
So the actual implementation for this in Bevy ends up becoming pretty gnarly, I

12:11.960 --> 12:19.560
don't recommend that your use case has to be this complicated, but for them they had specific

12:19.560 --> 12:25.080
lifetime needs for this parameter that I'm not going to get into how that works, you can

12:25.080 --> 12:30.200
dig into this or spend yourself. But this is just another example of how this technique is used

12:30.200 --> 12:39.560
to provide a much more flexible API in a wide-they-use game engine. So as I covered, this works really

12:39.560 --> 12:45.080
well if you want to implement dependency injection in your library, it allows your API to be much

12:45.080 --> 12:53.160
more flexible. And as a result of this flexibility, you end up with fewer functions to a smaller

12:53.240 --> 12:59.720
API surface to expose and internally because you're operating over a unified trait for

12:59.720 --> 13:03.400
different implementations, the internal implementation can also end up being simpler.

13:05.960 --> 13:15.320
And the last topic I'd like to cover is type of ratio. So you're probably familiar with the

13:15.320 --> 13:21.000
type of ratio that's provided by the standard library or by the language out of the box,

13:21.000 --> 13:28.520
and this is with the dying trait. And while this ends up or also known as trade objects,

13:28.520 --> 13:34.840
while this ends up working fairly well, it is limited in some ways. So for example, if you wanted

13:34.840 --> 13:43.000
a slice of type of raised objects, then you can't really erase a slice to be

13:43.000 --> 13:48.680
dying trait slice. That's just not a feature that's available right now in the language.

13:49.640 --> 13:56.840
And so what Devon does instead to get past this is implement its own VTable for type of

13:56.840 --> 14:03.880
ratio. So you end up having a bunch of different operations that are the first operations

14:03.880 --> 14:10.040
end up being for the identities. So displaying to users what types are you operating over,

14:10.920 --> 14:18.280
and then for the actual type ID to be able to distinguish, okay, when I'm actually

14:19.560 --> 14:23.640
recreating these objects, like are these actually the same type that I expect them to be,

14:24.440 --> 14:29.880
as well as if you want to co-locate objects to the same type, then this type ID ends up becoming important.

14:31.000 --> 14:39.320
I don't recommend using the address to an erase type or the address of the type ID function

14:39.320 --> 14:46.520
itself, which is something that some people do. Type ID itself is guaranteed to provide the uniqueness

14:46.520 --> 14:52.120
whereas if you're across different compilation units, you may end up with different addresses,

14:52.120 --> 14:59.000
but the same type ID. So do invoke the function of the type ID. And then we have other

15:00.200 --> 15:05.240
properties for memory management. So the actual size to determine, okay, when we're storing this

15:05.240 --> 15:13.320
in a slice, what's the actual stride? How far do you need to implement a pointer to actually

15:13.320 --> 15:19.800
iterate over a slice, and then for the allocating slices, we sort of function to provide that.

15:19.800 --> 15:28.120
And then actual functionality on these array objects hints at what's to come. We see that these

15:29.160 --> 15:33.720
actual provided values don't end up getting used. They have an underscore in front of them

15:34.440 --> 15:39.960
to hint that the values end up getting dropped. And the reason for that is we're going to magically

15:39.960 --> 15:48.040
transport these closures into the implementation. And the reason that this is legal is we can guarantee

15:48.040 --> 15:56.520
a compile time that the size of these closures is zero. And so because it's a zero size type as well

15:56.520 --> 16:04.200
as it's not a special token type, guaranteed by copies, send, sync, et cetera, and static,

16:05.320 --> 16:08.600
we can guarantee that we can actually just construct one of these out of thin error.

16:09.640 --> 16:18.040
And this is how you're able to go from a generic closure type to being able to create a function

16:18.040 --> 16:25.080
pointer that invokes it separately. So you can also use this technique to implement certain like

16:25.080 --> 16:35.240
FFI patterns. But yeah, it's a neat trick that ends up actually ironically simplifying a lot of

16:35.240 --> 16:43.640
the code I end up having in Devon. So to recap on type of ratio, by using custom V tables,

16:43.640 --> 16:51.000
you have a lot more fine control over the operations that you can do. So for example, to go back

16:51.000 --> 16:59.400
to what I was mentioning with these different operations. With Devon, you end up being able to,

17:02.120 --> 17:07.000
there's an option of, so for example, there's an option of bull here with the Equality function

17:07.000 --> 17:14.040
because what I'm not going to cover here, but one technique that you can do is you can use DRF specialization

17:14.120 --> 17:20.760
to check, okay, is this type actually equatable or doesn't not implement that? Likewise,

17:20.760 --> 17:27.400
you can use DRF specialization to pick DRF's display, and as well as checking if as

17:27.400 --> 17:36.520
RF to stirs possible. So that's why two of these functions are valuable, and the other one

17:36.520 --> 17:41.320
has the same signature between display and DRF. So ends up being the same regardless.

17:42.120 --> 17:49.320
But anyway, so that's why there's a lot more flexibility is just you're able to

17:50.120 --> 17:55.400
define exactly what the operations that are being provided are, not just based off of the direct

17:55.400 --> 18:01.240
trait implementations. However, this does require decent amount of unsafe codes, so use that

18:01.240 --> 18:06.200
you're on risk. I highly recommend running your code through Mary. If you're going to do this,

18:06.200 --> 18:10.520
it's able to catch memory leaks, undefined behavior. It's excellent to all.

18:12.680 --> 18:23.000
So these are all the techniques that I covered. They're definitely very niche, however, knowing

18:23.000 --> 18:30.360
that they exist can actually save your bacon. So I'm very happy that I've been able to take advantage

18:30.360 --> 18:35.080
of them, and maybe you all can come up with some personal stuff to do as well.

18:36.360 --> 18:39.800
And that is all. It's what they're quicker than I expected.

18:48.520 --> 18:56.280
And so I'm open-stating questions. So yeah, just one last thing, like I said, I'm looking for

18:56.280 --> 19:01.880
sponsorship or contributors on Devon. Can you please stay seated and feel the questions over? Thank you.

19:03.640 --> 19:09.320
So like I said at the beginning, I'm hoping to really make Devon a world-class benchmarking

19:09.320 --> 19:14.360
library, so I'm looking for sponsorship and contributors. So please reach out to me,

19:14.360 --> 19:21.880
either any of these by email, get Hub, pass it on. And yeah, otherwise, I'm happy to answer questions

19:21.880 --> 19:26.040
about anything I covered here. Are there any questions?

19:40.680 --> 19:48.280
So first off, awesome talk. And one of the issues I've often had with APIs that use

19:48.360 --> 19:54.520
apps like these is that the docs are really hard to us because you're only finding which values

19:54.520 --> 20:04.040
are okay, pass. And similarly, the compiler is often less helpful. Is there something you've faced

20:04.040 --> 20:12.920
in Devon and have you talked about it? Sorry, I can't hear the microphone. Is this better?

20:13.400 --> 20:22.040
A little bit. Okay, I think a little more, sorry. I will try to talk slowly. So often it's hard.

20:22.760 --> 20:29.960
If you're using an API like that uses these things to find out in the docs like which values are

20:29.960 --> 20:35.400
okay to pass on. And the compiler is often also an helpful. Is there something you've also

20:35.400 --> 20:40.920
encountered in Devon and is it something you've talked about? I'm sorry, please repeat the question

20:40.920 --> 20:47.400
one more time. You can actually very difficult to hear from here. Yeah, sorry. Often with APIs

20:47.400 --> 20:54.920
like these, the documents like documentation is hard to read because the types are difficult.

20:54.920 --> 21:00.840
And the compiler is often an helpful. Is this something you've also encountered in Devon and

21:00.840 --> 21:07.960
thought about? Sorry, the last thing I heard was compiler something. Sorry, it's so difficult.

21:11.800 --> 21:15.800
Because you've passed some functions there and stuff like this. And the compiler is

21:15.800 --> 21:21.240
sometimes it's unhelpful with it. So is there anything that you do not have to mitigate this or

21:21.240 --> 21:28.760
a documentage? Yeah, so unfortunately when it comes to resolving certain trade bounds, the compiler

21:28.760 --> 21:34.040
will, the Rust compiler will not be that much better than trying to use C++ templates where

21:34.120 --> 21:42.040
it will just try to leave you the wrong direction. I guess my advice here is if you do

21:42.040 --> 21:47.480
require trade bounds, try to require them, I guess as early as you need them, and that

21:48.600 --> 21:57.240
there are at least like no at the first use site, what's the associated issue? So for example,

21:57.240 --> 22:07.640
in the typestate builder example I gave, the name method didn't actually require that the

22:07.640 --> 22:13.480
end type implements into string, but perhaps it would actually be better to have that there because

22:13.480 --> 22:21.160
if you end up not invoking that name method or rather, if you end up invoking that name method

22:22.120 --> 22:26.360
the wrong way, then you get that error there instead of right at the part you called build.

22:27.240 --> 22:32.680
Aside from that, I don't know, you're just kind of at the mercy of the compiler giving good

22:32.680 --> 22:40.120
error messages. If you want good progress on that, go sponsor Esteban Cooper. He does a great job

22:40.120 --> 22:46.120
with improving the compiler messages. Any more questions?

22:51.320 --> 23:02.600
I think for your nice search, I have a question about your typestate example, so it's of course nice

23:02.600 --> 23:09.960
to have a possibility to fail at compile time when you use a pair incorrectly, but the error messages

23:09.960 --> 23:16.600
are not very nice, is there a way to improve them? The error messages are not very clear, so it

23:16.600 --> 23:23.480
doesn't say that your name is missing, it just says that type doesn't have a limitation.

23:24.840 --> 23:29.400
Yeah, I guess my answer to that is similar to the lot for strikes.

23:34.040 --> 23:39.480
I guess potentially that could be used to improve the error message when you end up calling

23:39.480 --> 23:48.120
build without the right trait impulse, so yeah theoretically that would help. I don't know

23:48.120 --> 23:53.720
about that, thank you. Thank you. Anybody else?

23:57.000 --> 24:02.040
No, thank you very much Nikolei. Thank you.

