Opposite of &'static

Suppose I have two not entirely hypothetical traits Deserialize<'de> and Deserializer<'de> with the semantics that if T: Deserialize<'de> and D: Deserializer<'de> then I can use a deserializer of type D to get out a value of type T.

fn do_the_thing<'de, T, D>(deserializer: D) -> T
where
    T: Deserialize<'de>,
    D: Deserializer<'de>;

Seems like the sort of thing somebody could build a library around. In any case, here are some impls:

impl<'de: 'a, 'a> Deserialize<'de> for &'a str { /* ... */ }

impl<'de> Deserialize<'de> for String { /* ... */ }

struct JsonDeserializer<'de> { input: &'de [u8] }
impl<'de> Deserializer<'de> for JsonDeserializer<'de> { /* ... */ }

This is great. We can make a JsonDeserializer<'de> and deserialize Strings from it regardless of 'de, and deserialize &'a strs from it as long as 'de outlives 'a.


But some deserializers only support deserializing types like String and not types like &'a str.

struct OtherDeserializer<R> { reader: R }
impl<R: io::Read> Deserializer<???> for OtherDeserializer<R> { /* ... */ }

There are basically two options for this impl.

impl<'de, R: io::Read> Deserializer<'de> for OtherDeserializer<R> { /* ... */ }

// or

impl<R: io::Read> Deserializer<'static> for OtherDeserializer<R> { /* ... */ }

They mean the same thing. In either case we can make an OtherDeserializer<R> and deserialize Strings from it which is great, but also we can (try to) deserialize any &str from it including &'static str.


The 'static in the last impl is the opposite of what we want. It means: assume this deserializer contains data that lives so long that it might as well live forever. The thing we want is: assume this deserializer contains data that lives so short that it never even exists in the first place.

impl<R: io::Read> Deserializer<'never> for OtherDeserializer<R> { /* ... */ }

I know about the postponed 'unsafe lifetime RFC but my understanding is it is addressing something very different. cc @jpernst anyway in case this has any bearing on a future iteration of 'unsafe.

I don’t plan to pursue this further but I figured it was worth writing down somewhere.

4 Likes

My understanding is that 'static does not mean “lives practically forever”, but rather “no matter how long this value lives it will not ever become or contain a dangling reference”, which is vacuously true for all types that don’t contain references at all even if they have short lives, and thus is exactly what you want in this case. This is why it makes sense for Box<A> to mean Box<A + 'static> by default, for example. If lifetimes represent sets of borrow checker constraints, then 'static is the empty set.

Of course, that’s not exactly obvious, and iirc 'static is not described that way in the official Book so I’m not confident that interpretation is going to stay correct forever, especially as the ergonomics of lifetime parameters get tweaked, so it would be nice to get an official clarification on this.

2 Likes

Yes T: 'static is a bound on the scope where holding a T value alive is allowed. It is your lease to keep it alive, not a mandate to actually keep it around for that whole scope.

For example String is 'static which means we have lease to keep the String values alive through any scope we want. We can also drop it before then.

Thanks @Ixrec and @bluss, I understand how that applies to bounds like T: 'static. But be careful because in our case these lifetimes are actually the inverse of how they normally work and 'static is almost definitely not what I want. Here is a more fleshed out example to show that.

trait Deserialize<'de>: Sized {
    fn here_have_a_transient_str(&str) -> Option<Self>;
    fn here_have_a_borrowed_str(&'de str) -> Option<Self>;
}

trait Deserializer<'de> {
    fn call_one_of_them<D>(self) -> Option<D> where D: Deserialize<'de>;
}

////////////////////////////////////////////////////////////////////////////////

impl<'de> Deserialize<'de> for String {
    fn here_have_a_transient_str(s: &str) -> Option<Self> { Some(s.to_owned()) }
    fn here_have_a_borrowed_str(s: &'de str) -> Option<Self> { Some(s.to_owned()) }
}

impl<'de: 'a, 'a> Deserialize<'de> for &'a str {
    fn here_have_a_transient_str(_: &str) -> Option<Self> {
        println!("no, this makes me sad :(");
        None
    }
    fn here_have_a_borrowed_str(s: &'de str) -> Option<Self> { Some(s) }
}

////////////////////////////////////////////////////////////////////////////////

struct DeserializerWithLifetime<'de> { input: &'de str }

impl<'de> Deserializer<'de> for DeserializerWithLifetime<'de> {
    fn call_one_of_them<D>(self) -> Option<D> where D: Deserialize<'de> {
        D::here_have_a_borrowed_str(self.input)
    }
}

struct DeserializerWithoutLifetime;

impl Deserializer<'static> for DeserializerWithoutLifetime {
    fn call_one_of_them<D>(self) -> Option<D> where D: Deserialize<'static> {
        let s = String::new();
        D::here_have_a_transient_str(&s)
    }
}

////////////////////////////////////////////////////////////////////////////////

fn main() {
    // Okay, deserialize a String by cloning the input.
    DeserializerWithLifetime { input: "" }.call_one_of_them::<String>();
    // Okay, deserialize a str by borrowing from the input.
    DeserializerWithLifetime { input: "" }.call_one_of_them::<&str>();
    // Okay, only allowed if the input is &'static.
    DeserializerWithLifetime { input: "" }.call_one_of_them::<&'static str>();
    // Correctly not allowed.
    //DeserializerWithLifetime { input: &"".to_owned() }.call_one_of_them::<&'static str>();

    // Okay, deserialize a String by cloning the transient string.
    DeserializerWithoutLifetime.call_one_of_them::<String>();
    // NO! DON'T LET ME CALL THIS!
    // The Deserializer's lifetime should act as though it is shorter than any
    // lifetime I can try to ask for - the opposite of 'static.
    DeserializerWithoutLifetime.call_one_of_them::<&str>();
    // EVEN WORSE!
    DeserializerWithoutLifetime.call_one_of_them::<&'static str>();
}
1 Like

The problem with the notion of 'shorter_than_any_lifetime is that the caller literally has no idea when the lease will expire, and therefore &'shorter_than_any_lifetime str is impossible to use safely, since for all you know it might’ve already expired!

@Fylwind that’s correct and that’s exactly the intended use case. The lifetime enforces the constraint that a string with that lifetime never exists. Check out the example code in the previous comment: DeserializerWithoutLifetime should not be allowed to call here_have_a_borrowed_str.

2 Likes

So, a lifetime analogue of Void?

2 Likes

Sure, that’s a good way to think about it. Whatever the name is, in the example above I would like to write:

impl Deserializer<'void> for DeserializerWithoutLifetime { /* ... */ }

And then be guaranteed the following:

// This compiles but the function is not able to call `here_have_a_borrowed_str`.
DeserializerWithoutLifetime.call_one_of_them::<String>();

// This does not compile because &str's impl would require 'void: 'a.
DeserializerWithoutLifetime.call_one_of_them::<&str>();

// This does not compile because &'static str's impl would require 'void: 'static.
DeserializerWithoutLifetime.call_one_of_them::<&'static str>();
1 Like

It doesn’t look like your example actually wants a “lifetime shorter than all lifetimes”. Rather it looks like the callee wants the ability to instantiate Deserialize<'de> with a specific lifetime of its choice, rather to allow the caller to choose. This means the signature of call_one_of_them should really be:

fn call_one_of_them<D>() -> Option<D> where D: for<'de> Deserialize<'de> {
                                            // ^^^^^^^^
    let s = String::new();
    D::here_have_a_borrowed_str(&s)
}

(Rust Playground)

This would allow D = String, but not D = &str nor D = &'static str.

1 Like

Correct, and that’s exactly what I do in serde_json::from_reader (the DeserializeOwned bound is just a simpler way of writing for<'de> Deserialize<'de>).

As you said, this solves the problem for people calling serde_json::from_reader. But it does not solve the problem for people interacting with a serde_json::Deserializer directly. I continue to believe that would require some opposite of 'static.

1 Like

Oh I don’t think the playground link solves the problem after all. That is equivalent to not having lifetimes at all (i.e. Serde 0.9). It would no longer be able to deserialize a &str even from deserializers that should support it.

1 Like

Can DeserializerWithLifetime and DeserializerWithoutLifetime be unified under a common trait interface without compromising DeserializerWithLifetime’s flexibility?

Suppose Rust has 'void as a lifetime. If 'a could be substituted by an arbitrary lifetime, then I could substitute 'a = 'void in the function:

fn run_deserializer<'a, D: Deserializer<'a>>(d: D) {
    d.call_one_of_them::<&str>();
}

Therefore, such a function should fail to compile. But this compiles on Rust stable, so it seems that 'void is of an entirely new category!

'void, that’s an interesting idea for expressing an arbitrarily short lifetime :thinking:

I understand this isn’t a pre-RFC or anything but I’m struggling to reconcile it with my current mental model of lifetimes. I guess right now we can say:

'a: 'a = true

And:

'static: 'a = true

Which is nice and simple.

My immediate thought for 'void is:

'void: 'a = false

That sounds about right, but then what about:

'void: 'void = ?

Which is where I gave up and ate some chips :sweat_smile: I suppose 'void needs more than just outlives to support it.

2 Likes

Definitely not a pre-RFC! Just flagging that Serde is less type safe than it could be because of something that I haven’t figured out yet. I think if I were to pursue this further, the next step would be using the intended behavior of the example code to work backwards and figure out how things like 'void: 'void = ? would need to work to get the intended behavior.

2 Likes

The compiler (internally) has a concept of 'empty ("the empty region") that is basically exactly this. We've never exposed it in user syntax, but I don't know of any reason not to do so.

6 Likes

What are the semantics of say:

fn inspect(i: &'empty i32) {
    println!("{}", i);
}

Would this be legal?

Do you mean 'a: 'void?

1 Like

@Fylwind I though that lead to the same contradiction but turns out you’re right.

'empty just happens to always be shorter than anything else because it’s for an arbitrarily short region. In that case 'empty: 'empty = true and you don’t need special rules for it, but it still fits in that model above.

As for that example, I would assume it would fail in the same way that this would:

fn inspect(i: &'notstatic i32) {
    print(i);
}

fn print(i: &'static i32) { ... }

i doesn’t live long enough to do anything with. In practice as a consumer I’d expect 'empty is something you end up with, rather than something you ask for. On the producing side I guess this would be ok?

fn do_the_thing() -> &'empty str {
    let s = String::new();
    &s
}

Looking at these examples in isolation makes the concept seem weird, but when you’ve got a series of traits like serde it looks valuable.

EDIT: Actually that second example looks pretty problematic, unless &'empty T coerced to ptr::null().

Under the compiler's current rules, that would not be legal, no. It would fail two rules:

  • First, a variable's type must be valid everywhere it is used, and i is valid nowhere.
  • Two, the types of arguments must be valid for the entire function call, and these types are not valid.

In general, Rust's current type rules do not allow you to have a variable whose type is invalid during its scope, so we would never allow any variable whose type referred to 'empty.

3 Likes

For reference here is my related idea/request: [Short lifetime, opposite of 'static? - The Rust Programming Language Forum] basically for a 'shortest lifetime', which seems to be mentioned in this thread aswell;

My idea is shortest rather than shorter than any ; the use cases are safe. In my mind it is for a simple use case: passing temporaries into functions; the functions may not cache or duplicated them, they may only pass them internally. Another way to express it is 'this reference cannot escape'

the recent case where it appeared for me: I was making a 'window type' that could accept a generic parameter for internal iteration of a hierarchy, but allowing that to be 'a tuple of references' (rather than 'T' and making the prototypes :&mut T etc) required the 'window type' to start specifying a manual lifetime parameter - even though this type was not part of the window, just part of an interface function.

Other than that I remember having to deal with lifetime annotations for another 'internal iterator' case (writing a 'zipWith(a,b,f)', a function taking a lambda, the lambda taking references to temporaries that the iterator-function would pass into it).

type SubWin = SubWindow<(&mut EdScene,&mut MTool)>; 
//<<ERROR: need to start making a lifetime parameter 
// part of the 'SubWIn' type, even though it's only intended as a temporary.. 
// the intent is there is never any 'PARAM' held persistently.

type BSubWin = Box<SubWin>;
trait SubWindow<PARAM> { //PARAM=app specific..
    fn ask_size(&self)->Option<ViewPos>{None} // how much space does it want,
    fn name(&self)->&str{"none"}
    fn event(&mut self, p:PARAM, r:Rect, e:Event)->Option<()>;
    fn render(&self, rc:&RC);

    fn foreach_child(&self, r:Option<Rect>, f:&FnMut(&SubWin,Option<Rect>)->());
    fn foreach_child_mut(&mut self, r:Option<Rect>, f:&FnMut(&mut SubWin,Option<Rect>)->());
}

.. I had a workaround but if I could directly express the intent of 'shortest or 'temporary or whatever the best name is, it would have been more elegant

On another note, it might be helpful to have a #[unsafe(borrow_checker_is_warnings)] - automatically mark all the functions in this module as unsafe, still let the borrow checker do it's work but make recommendations. Such code would not pollute the rust ecosystem as you'd still have to use an 'unsafe{}' block to call anything. then eventually we can meet in the middle once the ergonomics have been tweaked. Sometimes we have patterns that we know work because they come from tried and tested scenarios , but it's still hard to translate into rust; I do believe that it will be possible to make most scenarios easier to markup/specify than is possible at present;

regarding ease of reading and writing, and 'the number of mental steps to do something' - manually naming then tracking which names correspond to which item is more mental steps, and more to read past when trying to decipher what a piece of code actually does; I strongly believe a direct way of expressing the intent clearly would be superior - rather than 'heres some general labels; the context-specific use of these labels implies..'

I do realise that the manual labelling allows some more complex use-cases in what I call 'the middle ground' to be handled both efficiently and safely; thats great , when you need it.

I found the labelling of 'static slightly counter intuitive, but having read the explanation, I think it does handle some of my cases. Completing the range of simple cases with simple explicit labels would be great.

1 Like