How to emulate `super-let` on stable Rust

I want to read an HTTP response body from a cache file; if an error occurs while reading, the data is fetched from the internet.

The cached data and the fetched data have different types: the former is Cache, the latter is Body. To abstract over them, I defined a BodyRef struct that borrows the inner data.

I tried to construct a BodyRef with an if-let-else expression, but the code fails to compile because the lifetime of the fetched data is tied to the else branch.

How should I rewrite this code on stable Rust so the fetched data lives beyond the else branch?

/*
[dependencies]
chrono = { version = "0.4.42", features = ["serde"] }
reqwest = "0.12.28"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.146"
 */

use std::{io, str::FromStr};

use chrono::{DateTime, Utc};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::{Map as JsonMap, Value as JsonValue};

struct Body {
    map: JsonMap<String, JsonValue>,
    text: String,
}

struct BodyRef<'a> {
    map: &'a JsonMap<String, JsonValue>,
    text: &'a str,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(try_from = "CacheSerde")]
struct Cache {
    url: Url,
    date: DateTime<Utc>,
    text: String,
    #[serde(skip)] map: JsonMap<String, JsonValue>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CacheSerde {
    url: Url,
    date: DateTime<Utc>,
    text: String,
}

impl TryFrom<CacheSerde> for Cache {
    type Error = serde_json::Error;

    fn try_from(value: CacheSerde) -> Result<Self, Self::Error> {
        let body: JsonMap<String, JsonValue> = JsonMap::from_str(&value.text)?;
        Ok(Self {
            url: value.url,
            date: value.date,
            text: value.text,
            map: body,
        })
    }
}

fn read_cache() -> io::Result<Cache> { todo!() }

fn write_cache(_: &Cache) -> io::Result<()> { todo!() }

fn send_request() -> io::Result<Body> { todo!() }

fn main() {
    let cache: Option<Cache> = read_cache()
        .inspect_err(|e| {
            eprintln!("INFO: Failed to read cache. Fetching data from the network.\nCache error: {e}");
        })
        .ok();

    let body: BodyRef<'_> = if let Some(cache) = cache.as_ref() {
        BodyRef {
            map: &cache.map,
            text: &cache.text,
        }
    } else {
        // `super let` is experimental
        // super let body = send_request().unwrap();
        let body: Body = send_request().unwrap();

        BodyRef {
            // `body.map` does not live long enough borrowed value does not live long enough [E0597]
            map: &body.map,
            text: &body.text,
        }
    };

    dbg!(body.text);
}

Like this:

fn main() {
    let cache: Option<Cache> = read_cache()
        .inspect_err(|e| {
            eprintln!("INFO: Failed to read cache. Fetching data from the network.\nCache error: {e}");
        })
        .ok();

    let owned_body;
    let body: BodyRef<'_> = if let Some(cache) = cache.as_ref() {
        BodyRef {
            map: &cache.map,
            text: &cache.text,
        }
    } else {
        owned_body = send_request().unwrap();

        BodyRef {
            map: &owned_body.map,
            text: &owned_body.text,
        }
    };

    dbg!(body.text);
}

(super let is important for macros, but if it's not in a macro you can just move the declaration to the scope you need.)

6 Likes

Thanks. If I replace the if-let-else with a match, is it acceptable by common coding practice to declare multiple uninitialized variables, like this?

fn main() {
    let owned_cache: Cache;
    let owned_body: Body;
    let body: BodyRef<'_> = match read_cache() {
        Ok(cache) => {
            owned_cache = cache;
            BodyRef {
                map: &owned_cache.map,
                text: &owned_cache.text,
            }
        }
        Err(e) => {
            eprintln!(
                "INFO: Failed to read cache. Fetching data from the network.\nCache error: {e}"
            );
            owned_body = send_request().unwrap();
            BodyRef {
                map: &owned_body.map,
                text: &owned_body.text,
            }
        }
    };

    dbg!(body.text);
}

I'm not sure I can talk to how "common" it is, but wanted to point out that they're not really "uninitialized", more like "deferred initialization" if that makes sense?

You don't have to take the same care as uninitialized variables in C++ for example, here the compiler will loudly complain if you use the variable in a code path where it may not have been assigned to.

3 Likes

Absolutely.

(Assuming that you actually need the scoping difference. If you don't, then you should declare-and-initialize at the same time instead, obviously.)

2 Likes