Skip to content

Latest commit

 

History

History
146 lines (74 loc) · 35.1 KB

File metadata and controls

146 lines (74 loc) · 35.1 KB

I sketched a whole new version of this talk...then just recorded and jammed over the handwritten notes.

Here's the transcript:

CorrectByConstructionJam.m4a

Hi, everyone. Welcome to my talk. My talk is called correct by construction, and it's all about trying to write software in a way that means if it compiles, it's probably right and probably working. This talk has come about from experiences I've had as I've journeyed through my career and as I've realized more and more of the powerful constructs that C++ gives us and how they can be used to ensure that the programs you write are just right.

As in just correct. If they compile, there are a number of techniques that we'll talk about today and hopefully they'll give you some ideas to take away and use for yourself in your own programs to make them more correct and hopefully catch errors earlier. Obviously, one aspect of programming is mistakes. We all make them think it's fairly well understood that the earlier you catch a mistake, the easier it is to fix. So, of course, having assertions in your code, having exceptions and checking for exceptional cases that are mistakes in the program. That's a great thing to do, but it's often too late. If you're into your car decides that it can't can't stop anymore, it's no real use to you seeing that the brake pedal doesn't work with an exception on your screen. You want to you want it to fail early. Before I did before you start the car to say, hey, that the breakstone where they drive off. So. If we can make our programs only compile correctly and not get errors from the compiler, if they are bug free or at least intention free, then obviously that's a good thing.

Who am I? Well, I am Matt Gobbo.

I have been programming and C++ now for 28 years and my experiences have taken me through the video games industry where performance was really critical. And second, you know, in the era that I was in the industry, there was no patching of games and downloading updates. You had to get it right because it was going to be put on a DVD and shipped to.

Shipped to distributors. And so there was no sort of do over. So everything had to work as best you possibly could. Obviously, that we still had bugs. Some really fun ones, which I can talk about another time. But that was my first 10 years of my career. Then I spent some time do some consultancy work and writing C++ tools, which was fun. Gave me a whole appreciation for the language and what you could do with it. I spent a couple of years at Google and their YouTube app and then most recently I've been in finance doing high speed, mostly high speed.

Finance programs where. What has to be correct? It's very expensive. If you make mistakes. But one also has to be performant because you're in an arms race with other companies doing the same thing.

So you can kind of see how the shape of my career maybe has drawn brought me to this conclusion. I need to have performance code and need to be as little as possible chance for it to go wrong at runtime. The first technique I'm going to talk about is kind of I would argue one of C++ is killer features and other languages have it, too. Don't get me wrong, this is not a C++ is perfect for programming kind of a talk. But I want to talk to you about using strong types. Obviously, there are a number of language choices that one can make where the strength of the tape is is, well, either non-existence like JavaScript and Pythons of this world or very weak like Java to some extent has a slightly weaker type system. But C++, for better or worse, has quite a strong idea about what a type is. So I want to show you some code, the kind of code that maybe I would have written in one time. And so here we're going to imagine we're still in the fund. I'm of in the finance industry and I'm writing an API and the API is going to send an order into the market.

It's going to say, hey, I'd love to buy some Google. I'd like to buy 100 shares of Google. And I will pay one hundred and fifty dollars for it. So one way to write an API would look like this. And I'm sure you've all seen code like this. You've written code like this. And of course, this is correct. But I challenge you to find one to sell those Google shares in the next slide. Can you remember? Is this right? This could be a very expensive mistake. If I've got these the wrong way round, then instead of buying Google shares, I'm selling them or I'm I'm instead of buying 100 shares for nine hundred and ninety. So I selling 100 shares for nine hundred ninety dollars. I'm selling nine hundred ninety shares or one hundred dollars. That's a very different thing. And you could see that this kind of thing would be a dreadful thing to happen, especially when you can make thousands of trades per millisecond. So don't do this. How can we fix it? Does this fix it? Note to self, this is when I would use TYPEDEF and show that it doesn't fix it.

Typedef and using they don't fix it.

No. Neither of these two things introduce an actual new strong type. And that's an unfortunate thing because really what I want to do is write this.

And I want the compiler to tell me that I've got it wrong. That's a much better and much clearer thing. Piece of code, right? This error tells me that I've got it wrong and tells me why I got it wrong.

And the, quote, code won't compile. I've got it this way round. So how can I write a strong type?

Well, first of all, note that the performance of the code is the same, whether I use it end or the strong type here. That's a really important thing when you're in finance, you know. Or indeed, any kind of case that this doesn't cost us anything. It's a computer key compile time conceit to have this extra wrapping around it. Then there are ways you can use boost something, something here, boost or jonathon boccaro’s fluid stuff. https://github.com/joboccara/NamedType

I tend to write them directly myself. And I have just the things that it makes sense to have. I like taking a price and adding another price to it makes sense. Taking a price, adding a quantity doesn't make sense. Multiplying a price on a quantity together does make sense. Now I put like a total cost. And that's not a price. It's a cost. And that can be another type of object. So I can make a type algebra that makes sense in my . And that algebra is compile time checked. And I can only I can pass costs into things that expect costs instead of passing prices and quantities or just make mixing them up. So, of course, here's another thing here. I mean, I wrote this trade function and it takes a boolean parameter to buy or sell. Now, let's assume that's a recipe for disaster, as I'm sure you all appreciate.

It's very easy to forget. Is it true to buy true to sell bool? I don't know. Maybe I should look it up. Obviously, you can give it a decent name. The parameter name. But here's the problem. The parameter name won't save you. A refactor can change the name of the parameter, can change the meaning of the parameter, but it won't update the code that uses it. One very good suggestion here would be to use, buy and sell, like actually change the API into two calls and hide away his true or false. That's going in a little inline functions. And now it's very clear to the reader that you buy the buying or selling shares. Really important to get those around, as I'm sure you appreciate. If there were more than two or indeed if it would make sense to pass through the bindis or to sell this through multiple chains of execution, then one might reasonably still want to have it as a parameter rather than a different function. Otherwise, you'll find yourself using like an if statement around, you know, if I want to buy them by ourselves. So and that's annoying. So it's useful to have it as a parameter, but what can we do instead? Well, the obvious choice here is to use. And enough. And let's use a nice name that has an impasse, and then we now we have like a trade type trade type buy or trade type sell every time you see this. You can tell that you've got the parameter the right way round because it's not true or false anymore as buy or sell. And that makes a lot more sense. Similarly, if I have multiple parameters, maybe there's a short sale or not short sale or something else like that that I need to dream up. Then you can add more parameters and each one is strongly tied. They're all different from each other. So I can't switch around by accident and posit true, false, single, false, true. It's always clear what I'm doing. Each time I'm trading. And this is a short trade and selling and it's selling short.

So this is a pretty simple thing to do. There was a bit more work involved in writing our price and quantity classes, but they're useful abstractions. They make sense. We can embed scales in them. We can have the algebra of knowing what a cost is compared to the price or quantity or notional value or any any of those things. That makes sense in the domain that I'm in. And I get this protection and importantly, the code comes out exactly the same. So I'm not costing myself runtime performance. It's all upside. Maybe a little bit more work on the programming side of things.

nother thing that one might do in finance. Decode market data. So when you're writing a trading system, you're forever consuming this giant stream of modifications. So you have this idea about what the market looks like at some point in time, the price of Google, the price of Amazon to buy and sell, all those kinds of things. And then the way that you keep your information up to date about the state of the market is you're given these changes, these modifications. And they will tell you whether some other person in the market is either adding some interest to buy or sell, is modifying their existing interest or they're removing their interest, or already that is trigger a trade that's occurred. So this is known as market data and market, it was a stream of these updates.

It's like a big distributed database that you're getting updates to tell you how to update your idea of the world to match what the true state of the world is. And these coming thick and fast, they're very important to get right, obviously. And so does a binary format. And very often it shows his age has Maskey hiding inside of it. And so, quite reasonably, you would see things like this, you know, like the ad isn't a device. And this is in the packet that you've decoded that's come from the network. And I've seen trading systems that pass this name through not unreasonably. And this is this is very straightforward to see that you've got an A for an end to modify and whatnot, and you can then pass it through our system and all as well, because if statements around a switch statement here and we put a default here because, you know, Wolf, we kind of have to the compiler does it. I don't know if it does. We'll see. Of course, it's easy to type out this. That's one problem. What if I know I used a lower case, I have an upper case, I. Well, I mean, that's very unlikely to stay. Hopefully my test will find it. But can I make this can I change us in a way that means that the compiler helps me? Well, yes, I can.

Now, I can't do everything at compile time. I can do is at the very edge when I decode my packet, when I read it, instead of using and passing through that character, literal or string literal, which I've seen used in many cases and other pieces, pieces of code, I can switch on it and turn into one of my names. And now I have some powerful construct. I know there is one place I need to test. This is the edge data comes in. It's either A and D or T, and I turn it into one of the four enum values. And this is the. If I get something outside of about now, it is a runtime error. I have to do with it there. But in the rest of my code, I have sanitized it as an add on costs. That means I can use the fact that the compilers these days will error and warn me award rather than error. If I forget to had one of the cases, what I like to do is turn things into names as fast as possible on the edge. Now I have something I could reason about within my code base, and I typically don't use defaults. And Mike, in my switch statements and I would recommend not doing these. If you think about what would it mean if this particular piece of information got to this point in my code? Very importantly, that also means that if as a result of updating the code, maybe a new version of the market, it comes out and there is a different type of information that's now flowing.

I can add to my Ingunn, hey, there's this new type of thing, which is no market, Holte H. And now, again, era and every piece of code where I was switching, where I was making a decision about what to do with a piece of market data where I haven't handled this whole condition. And that's really powerful. That means that I can I can find quickly those locations that need to be updated where I have to say, oh, what would happen if a hole got to this part of the code? That's that's a powerful thing to be able to take advantage of. And it's a useful thing. So I would encourage you all to do the same. So use the name Cloches. Try not to use default unless you're absolutely certain that all the cases you will ever need to cover are cow covered and then everything else is fine. I guess the takeaway of this section is: use strong types. Define algebra's between them, use strong in some classes, don't use in slopes, doubles pools unless you can avoid them right there. Unless that makes perfect sense where they are, you should just try and find domain objects to map your domain perfectly. That's this terrible.

Not everything, of course, is a simple sort of value type that can be a strong flight. Well, we've been talk about so far a sort of value that you date. They don't mean anything with them, so they don't refer to anything. They they can be copied cheaply. They're just an integer or a double or whatever. Sometimes you need to do something with objects that are a bit more far reaching. So what I told you about. All right, I. This is not a good Segway. We talked about this in a better way. But we segway into it, we all know what remove all hopefully seen eye. One of the worst named aspects in C++ is this also known as Cordray. For COMPILER, a complimentary construct requires Destructor releases or S.B are and scope based resource management, which to me makes a lot more sense. But it talks about resources. So anyway, we hear that. So an acquisition releasing its resources were acquiring or resources that we're releasing and, you know, initializing that kind of stuff. But it's not just for them. But let's first of all, let's just go over what. All right. I guess most of us have used it was a way now that, you know, naked allocations are a bad thing. We should take our news and delete and throw them in the bin and use unique pointers. Very occasionally. We might use SharePoint pointers, but. That is not necessarily all the RIAA is useful for. Of course, is extremely valuable to use. All right. I hate to make sure that those things are eradicated and cleaned up correctly, but we can use, in fact, much more. So, again, drawing from my experiences in finance, oftentimes we have to interface with strange and exotic hardware, which is a lot of fun. I really enjoy playing around with these things, but hardware is another sort of set of resources and oftentimes there are c API is wrapping the code. Hello. More recordings back after brunch first. I call that I was talking about Ari II and how it isn't just used for resources.

And then I immediately came up with another resource. So resources aren't just memory. They can be resources, actual resources like. On a system like an accelerated network card, there may be a certain number of slots where you can preserve and allocate. Package that can be put onto the card, not in the memory of your computer, but actually uploaded to the card and ready to go, ready to be sent out. And that's it. Obviously a fairly standard thing to be able to do. Maybe you have a C API that allows you to allocate and D allocate and fill those buffers. So one could obviously just call the C API. That's one of the most obvious things to do. But using ALRE II, we can go better. We can wrap the handle the whatever all C API gives us with some objects that represent the allocated resource on the card. That way we can know that one can always ensure that the resources are cleaned up afterwards. Once we've wrapped and now we know that we haven't got any leaks, we have to be careful.

We can perhaps use unique pointer.

Because behind the scenes, I can have a different allocation than than here behind the scenes, and then that means that we get the right to potentially the right semantics for eradications. That means that we don't have to write our own, r.i, wrapper. But it's not too difficult to write your own wrap up. There's some interesting side effects of this. We can choose to model our relationship between things.

For example, it may be that you need to allocate lots of initialize the graphics card, not the graphics card, isn't the network card and get like a handle to the network of the CIPA. Maybe you like in it card get the handle and then using that handle the card handle. You can get buffer handles and those buffer handles, then you can fill with data and send them whatever you need to do.

There's therefore a link between the buffer's and the handle of the card. One might reasonably choose to use a shared pointer to say these buffers need also a reference back to a need to keep the card alive for want of a better word.

So one way would be to follow the shared pointer. I'm not so sure about that, although it would be sort of correct by construction and in the strictest sense. It would mean that there's a sort of unnecessary burden on remembering to clear up all of the the buffers. If that's unnecessary. Now, I'm saying it out loud, above all the buffers before you could shut down the card. Maybe that's right. Other times, though, you might be able to correct by construction of your program and show that the card is initialized right at the beginning and the initialized at the end. So there are different arguments for this. What money might want to do is use a weak pointer. Perhaps a weak point will mean that I can check to make sure that the card hasn't gone out of scope. Without actually making it go out of scope or I can. Can have a count of the number of buffers in use and make sure that the deter an error. Nominees are that appealing and they should point to one sort of make sense. So that's obviously a resource type. And we've been able to wrap a C API to make it safe to use, if not totally too volatile with. What about other things? How often do you find yourself writing a piece of code where you need to have add a sort of listener? You know, you subscribe to some kind of event in an event system, for example, maybe it may be many parts of your program need to know about the delivery of a packet. And having received the packet, is the responsibility of the networks right. To call all of the listeners and say here is a new packet. Of course, your. For every subscription you add, you need to ensure that when you go away, that you unsubscribe to this sort of pair of subscribe and unsubscribe and sort of an unwritten contract or maybe even a written contract, but written in comments that says, hey, make sure you unsubscribe before your object goes away.

So this isn't a resource in the strictest sense of a resource. It's not like we're allocating or reallocating something.

This is a pair of operations that must happen at a relationship between objects that must be held. This is a precondition or a post conditional? Both.

A way to ensure that under all circumstances, all under all circumstances, unsubscribe is called before an object goes away, would be to use some kind of, ah, a I style object so I can hold a subscription by this this subscription object by by dint of being constructed, subscribes my object to an event. And then this subscription object by when it falls out of scope, unsubscribes me from that event. And now I protect myself from either double unsubscribing, subscribing multiple times, or just forgetting to unsubscribe. And I've used oray i style techniques or scoped base resource management for something which isn't really a resource. Don't forget that it's an important aspect that you can use these techniques for things that aren't resources despite the name.

Another way that you can use these kinds of things is. Take this piece of code here, we're iterating over. The list of subscribers and were calling each one to say, hey, an event has happened. We've all seen code like this. There's one thing that can happen here that is this potentially problematic. This is actually a good answer. Potential argument for using a list. Is that a bit better? What if while the iterating over those subscribers, one of them decides to unsubscribe? Now, we've got a problem. Now, potentially this vector that we're iterating over is being mutated unbeknownst to us. Now, this is one of the bigger problems, of course, with this approach. Yes. Subscribers and subscribers and things. One solution is to make a list. Another solution is to take a copy of this vector before we it's right over it. That has a side effect of now it's on unsubscribes as a result or as a result of a packet. A different subscription is removed. Actually, we still might call the other subscription. That's a problem.

But we can't have people be taking it. How might we detect the modification?

Of this, how might we? I mean, one way is that we could have like a flag and we sent a flag to say, hey, look, we're walking the list right now.

Anyone who tries to delete this, please throw an exception. And then in our unsubscribed method, we check that and say, look, hey, you can't unsubscribe while the subscription is going off.

That's kind of annoying, right?

Another way. I mean, first of all, but but lessness, lessness, stay with it. So now we have to say it's true at the beginning and pulser then. And no matter what, we have to make sure that the false line in the bottom here. Sorry, when we have however we exit this situation that we must set it back to force and otherwise no one could ever unsubscribe again. So here's another example where we could use a little. All right. I object here and say, OK, while this thing is in play. Nobody can unsubscribe. And then when it falls out of scope, naturally and subscriptions resume, that's cool. But we can go on further. What if we. Instead of unsubscribing?

Accurate. Instead of preventing on subscription, we accumulate inside this object things that will be added to unsubscribed at the end. Well, the destructor of this object can then apply those on subscriptions. Now we've got some level of of. Something, something, something. And so here we've used II again, not because it's managing a resource, but because it guarantees a pair of things that happen, one construction and one of destruction. And it does matter how we get out of here. We're going to be OK. We're going to have our invariance held. There's another example, and this is more like a resource. Another example of this kind of technique is when you have a new tax, you want to lock the mutex decent protected work and then unlock the meetings. There is already in the SDL something which will do this for you as an hour I II style object and that is unique local scope lock. We're not gonna go into what the differences are here, but the constructor acquires the lock and the destructor releases the lock and that's really useful.

The problem I've seen. With sometimes it's hard to design an API where holding a lock and doing a bunch of things, that of atomically is easy to phrase. So we'll talk a little bit about techniques that you can use for that. What do I mean? So imagine you have some complicated state inside of an object and you really ideally like to someone to come in an external piece of code to come in, mutate that object and but atomically and mutations that may involve multiple interactions with the objects in adding lots of things, calling math multiple methods. So you might have a lock and an unlock method on your actual object. I need a more concrete example here that lock and unlock. Obviously, we'll have this sort of pre and post conditions, as you must remember, if you call a lock to unlock so we can make a little lock object that is responsible for for holding that lock. But then we have this bunch of methods in the middle that say make sure you're holding the lock. Anytime you have a comment that says something like make sure or ensure or must have tried to think hard, maybe there's a way to take that comment and turn it into a piece of code or a constructed the compiler can check for you. One way to do this is to instead of just using a unique lock or a scope lock is hide away, the mutates here are gone.

Those mutations are no longer part of the object. You can't. Access the object anymore. You can't take the object and talk. The only way you could take this object is by getting the lock object and the lock object is a friend. Of the original object, and it exposes all of the mutates. So now we've got a situation where I can query the object all day long. Internally, a lot will be taken out. We'll get the results we expect. But if I want to mutate the object of a lot of mutated objects in a complicated way, I have to get this mutated lock and then you take a look. Isn't just an R.A. Istar object that's holding the lock. It is also the keys that allow me to get inside. There's a bound together. So it is impossible for me to have a way of mutating the object without holding the lock. Now, that is exactly the kind of thing I mean, when I talk about constrict, correct by construction. I can't do the things that are dangerous anymore. There's no more comments explaining. This is like you just have to get the mutated while you have the mutator. The object is locked. You take the object to you, like throw the mutator away. You're done. So that's one approach.

So nu te tau.

Another API you might consider is if you can bundle together all of the changes, do you want to make to the object into a single thing, single object, a change object, a transactional object, then you could have a single case apply. So now you're no longer able to hold the lock. You're no longer sorry, you can't go and get the lock and you can't be. But what you can do is give a list of mutations to the object and the object will hold its own lock while it applies all of your mutations and then unlock. So now, again, we've we've changed the way the code is written because I can now poke around and read and write and whatever. Yes, but. I can't do reading and writing whatever, but if it makes sense to me to apply an atomic transaction, then that's the level that I would I would apply. And then another thing I found useful from time to time. If you need a bit more of a general case back and forth with the object and and more importantly, if you need to read and write and maybe the.

All right, I and alternative to the right eye style is to have a something like this where you run under lock and I'm going to pass a lambda to the object and say, hey, lock yourself, then call this lambda with a mutable reference to all the things that it can change and to read and write to his heart's content because you have the lock.

And then when it returns, having mutated, the object is being given. You'll release the lock. And now we've got a situation where, again, I can't get at this inner sanctum the secret in part of the inside of the object unless I am the correct locking around it. And there's just no apologetic comment. It just works. Now, I need to talk a little bit about Constance, this. This is where it's going to fall down a bit more. But so let's talk out loud and see where we go. While we're talking about pre and post conditions. What do I mean? I mean, things that I just wants to be true or needs to be true. Sometimes there are things that are convenient to be true and some things are actually must be true.

When I'm reading code, it's nice to make assumptions about things.

If I assign a value to a variable, it's nice to be able to make the generalization of or understand whether that variable is going to change during the course of a function while I'm reading it, or whether it's just a value that I've given a name to. Very often, variables don't vary. Variables are actually just convenient names.

Let me write that down. That's good. Variables don't often variables don't vary.

We'll put that to there just in a convenient name for an in an intermediate calculation so that a human can come in and read it. We all know that compilers of Britain are reading through our intentions and working out what we actually meant. So this doesn't necessarily carry any overhead, but some it's useful, useful to give a name to these things. But if the variable isn't designed to vary Navy, I should say that it doesn't vary. So I want to make it cost. This is doesn't make it costs. I'm giving myself as myself the reader. Ten minutes later, a big clue. This is just a name for something that a convenient name that I want to give to something in the middle of my function.

So I think it makes sense to make as many things as possible, cost to aid readability and to reduce the burden of determining what things are actual variables. Here's an example piece of code where for whatever reason, we have decided to encash the first few Fibonacci numbers. It's a pain to calculate the Fibonacci sequence.

So one reason we might just make a little table like this, call a function a few times and then you might notice, for whatever reason, the computer isn't smart enough. It should be there with me. This is like it should be smart enough.

But let's assume that we want to really make it obvious. We want to make sure that this table is calculated. Sundara A table. What can I do?

There's gonna be a table I can do table table y u v.

I want to return a giant array of things. And I don't want to just write them out longhand. So I want the first thousand Fibonacci numbers here. And I can make a method that does it. I don't really want to. It's here. I want to define it in line. But if I define it in line, first of all, the compiler will do it every time. And I know what that I can make it static and make it static. Then the compiler will forever be checking to see if. Is this the first time that I've called this function and if I've called the function the first time that I need to initialize it. But equally, I don't want to do that work and I don't want to make it mutable, which would have to be. So how can I make this the best of all worlds? How can I make it constant but still right. Rational, sensible code to calculate at compile time all the things I need to calculate.

Or maybe even a runtime. But make it constant. Well, here's one what we can call into an immediately invoked function expression. I f e.

That can then initialise do whatever it likes. But return something which will then go into our constant variable. And now we have got we've Retek, we've trained, we've retained constancy without having to define a little out of line function. We've kept the code where we wanted the code to be. And hurray. Let's talk. That's gonna have to work a bit more on that.

So you take the low tech boom, boom, boom, boom, boom.

You left that out. So we need to do the purge. Isn't that better on this? While iterating.

Wants to come Konsta, this sort of feeds into another pattern, which is maybe it does make sense to have lots of knobs and buttons to twist on so important. So, for example, let's take a UI component. I've got a representation of something which is a button and buttons can have any number of parameters of controlling how they look, how big they are. So there will be all these mutation methods on my bottom and that might make sense. But what it might be, I want to make them face. I want to be I have a spot at the end of this or I want to apply all of my my changes and then sort of bake it into the finished article, which is now complete. Now I have myosin, a little H.T. email that represents my bottom and I can't look me take it off because I don't want to take it afterwards. It makes it. Now I can make it count. So obviously I could use a little IFAD to kind of configure some all of the attributes of my button and then return it.

But this is more go away.

This is more of a pattern for a builder. Another thing that Bill does allow you to do is does this fit? And if this fits but a build a pattern where you use a different type of object from the final object that accumulates all of the changes that you want to make and then you could build on it to return the actual finished article. The builder object can have a lot of defaults. So if you're a fan of having a simple to configure objects, you know, maybe a button's default form is Helvetica or whatever, then, you know, that can be your default parameter. You don't have to have thousands of parameters on your object because you either have to have a million different types that we discussed before or else we have to name them. And if you called dot set name and linked, since this may maybe or maybe build a.

Patton, questionmark.

Kutty why testing's do something with quantities Kutty Why that kind of stuff. Let's talk a bit about that and then we're done something. Build a. Really handy for testing. If you need to be able to have a kind of example, objects with only a small mutation to really exercise a particular aspect in a test, then the builder pattern can be your friend. And if are using strong types, you can even have a very literate way of of configuring your your tests.

You can have, you know, button Perin button builder or button code to build up print Randolf with a new bracket width of 10 ways, blah blah blah. And you overload on on the ways to allow you to pick out quite bright aspects of the other. But you've mutating and you're using the strong pipestem. So that becomes quite a good way with user defined expressions, user defined, user defined literals.

This can look quite nice in tests.

Obviously, you can do the same thing with different parameters. But but this is one way around it anyway. That is about all I've got in the tank right now. So in conclusion.

Your compiler can with the right techniques, the compiler can help you make sure that your code is is right, it can help you by ensuring your types are correct. You're passing the parameters in the right order. It can obviate the need to pass parameters in the cases where defaults make sense. It can ensure that resources are required and destroyed in the right way and that they're shared appropriately. Moreover, the techniques that you use for resource acquisition and destruction scale to things that aren't just true, resources that are relationships of pre and post, conditions between objects. You can ensure that the compiler enforces the contracts of constancy where it reduces the burden, the cognitive burden on the programmer reading the code to determine which things can change and which things can't change during the execution of the function. And he could do all of these things without affecting performance in any meaningful way. So I hope that with some of these techniques, you can go back to your code and ensure that your program is correct by construction. Thank you.

More bloody notes:

That lot was 45m of talking. “Principle of least surprise” Note references vs value types method& and method&& ?