Skip to content
No description, website, or topics provided.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.


I'm going to look at lifetimes from a different angle, from the point of view of dependencies and encapsulation.

The first quesion we want to answer is 'what is a lifetime?' but I will need to build a foundation first to answer this question.

Also, while we are actually coding, we want to be able to easily understand how to read code that has lifetimes notation.


We can easily read this function. 'the function takes a u32 and returns a u32'.


But how do we say this function? 'the function takes a mutable reference to a u32 and returns a mutable reference to a u32'. But we haven't included the lifetime notation in our description.

Without actually using the word 'lifetime', how would we read this function?


So these are our goals,


to create a good definition of 'lifetime' and


find an easy wat to talk and think about lifetimes.


Lets lets build things up using dependences and encapsulation. This is a function with no input and output


Its completely encapsulated. It has no depdendences. Its not very useful.


This is a function which takes a u32 and outputs a u32. So now we have a dependency. It is controlled in that only a u32 is allowed, so the function has some requirements of the outside world - it won't compile unless the dependency is a u32.


We can look at this in a graphically.


We have to make some kind of assumptions about the outside world, over which we have no control.


How about this function? In fact we can look at the angle brackets also as a kind of dependency. Before the function is compiled we don't know what the type of T will be. The output of the function will depend on the condition of the outside world so we have a dependency.


In fact, this dependency is resolved at compile-time. Before the function is compiled, we don't know T. After compilation, T is full determined. So now we have a complete picture of the dependencies of this function. There are both run-time dependences and compile-time dependencies.


From a more graphical point of view, at compile time, the function chooses either struct A or struct B.


Now we'll look at the idea of a reference in terms of dependencies. What is the difference between these two functions?


The top function fully pulls in the u32 into its encapsulation boundary. In this way the u32 is fully controlled, within the function. The second function just leaves the u32 in the outside world and uses it from there. The second function is taking a risk, because the function can't control what happens in the outside world - its not within its encapsulation boundary. So its possible that something else comes along and uses the u32 while the function is u32.

Why would we take this risk? Its a compromise between encapsulation and speed/efficiency. It might be costly to copy data from the outside world into our function if the data size is large.

So the reference notation says "I'll take the risk. Something might happen to the outside world, but I don't want to go to all the trouble of copying, so I'm just going to hope nothing comes along and changes it."


Actually Rusts borrow system controls this risk. The borrow system rules set some expectations about the outside world that the function is allowed to have. The borrow systems rules guarantee that, even if we leave the data in the outside world and don't copy it into our function, we know that that data won't change while the function is using it. That's because either its a shared reference, in which case no other function can change its value either, or its a mutable reference, in which case there is only one reference, and that is the reference our function has to the data, in which case our function has total control over the data. In either case, we are guaranteed that the value won't change while our function is executing. Its gives us determinism.


We'll use the term 'referand' to refer to the data that our reference is pointing to. We'll use 'reference' to mean the address value that points to the referand.


The borrow system creates some good expectation that a function can have about the outside world.

The problem is that the borrow system rules don't guarantee that the referand exists.


so the function has to take responsibility for the existence of the referand.


so we need some syntax in our function to describe this responsibility.


Something that says "the function assumes that the referand to this &mut u32 exists".


Lifetimes are resolved at compile-time, so this needs to be expressed as a compile-time dependency, so it should get written into the angle brackets, like <'a>.

<'> means the function has the requirement that the referand to the references in our function exists.

Which references? We need to link up the referand <'a> to the references in the function arguments, so we use <'a> and &'a to join up the referand with the references. Lifetime elision gives us some rules so that this linking often doesn't need to be explicit.


So now we have built things up to the point where we have a definition...


Here is the definition. If we really want to pull a function apart in detail, we can now read off the function definition below as words.


How is this useful?


When we write code its better for all the components to be loosely coupled. Each component has good encapsulation is independent from other components except through a clearly defined interface. Dependences are limited. This means we can plug in new components as we want, making the whole system scalable, and not requiring so much up-front design.


The problem is that as soon as we have references in our functions, we have lifetime dependencies, which require the existence on one component conditional on the existence of another component.


So when we have components linked by references, a better way of thinking about the is a nested/hierarchical/tree structure. One of the implications of this is that we need to do lots more up-front design - we need to spend more mental effort figuring out how data-structures and functions embed in one another.


These components can still link to others when we have no references.


In summary, when we have no references the loosely coupled model works well. When we have references we need to start thinking of nested components, which require a lot more up-front design. We can no longer just plug in new components, we need to design the whole structure early on in the design process.

You can’t perform that action at this time.