#Translating TypeScript-esque optional semantics into Rust

16 messages · Page 1 of 1 (latest)

low tide
#

Hi there!

I'm getting stuck trying to work with nested Option<T>. Let's say I have the following struct:

#[derive(Deserialize, Serialize)]
struct MyAppConfig {
  port: u16,
  api_key: String,
  private_key: Option<String>,
  file_roots: HashMap<String, PathBuf>,
  module_config: SomeSubModuleConfig, // I'll skip defining this one here
}  

I read the file with serde in some load_config function or something similar and return a Option<MyAppConfig> from there. Now we get to my problem: how do I access the fields there ergonomically? I referenced TypeScript in my question title because my brain (which is very used to thinking in "the TypeScript way") wants to do something like the following:

const apiKey = config?.apiKey;
if (!apiKey) throw Error(…);
console.log(`Using api key: ${apiKey}`);
const privateKey = config?.privateKey ?? generateNewPrivateKey();
console.log(`Using private key: ${hash(privateKey)}`);

And with Rust I'm trying to translate this to something like (trying to avoid .clone()):

const api_key = config.map(|c| c.apiKey).take().unwrap_or_else(generateNewApiKey);
const private_key = config.map(|c| private_key).flatten().take().unwrap_or_else(generateNewPrivateKey);

But all my attempts at wrangling this into code that'd pass the borrow checking fail.

I don't like the idea of doing a let Some(config) = config with two paths, since then I'm writing the code that calls the generateNewKey functions twice. So, any pointers on how to do this ergonomically?

low tide
#

Whew, I have the following working now:

// For fields that are `Option<T>`:
config
  .as_mut()
  .and_then(|c| c.private_key.take())
  .take()
  .unwrap_or_else(generateNewPrivateKey);

// For fields that are just `T`:
config
  .as_mut()
  .map(|c| std::mem::take(&mut c.api_key))
  .unwrap_or_else(generateNewApiKey);

But this is still really rather wordy for what it's doing so I'd still love to see a nicer-to-read version of this all

fluid gorge
#

I can give you one small shorthand: .map().flatten() is equivalent to and_then, but other than that I have no cool workarounds

wheat edge
#

Do you need to modify the private key? You might be alright using a reference to the string instead of taking ownership of it

fluid gorge
#

If your problem is just consuming config twice, you might have some luck with let (api_key, private_key) = config.map(|c| (c.apiKey, c.private_key())).unzip()

low tide
#

No it's not just twice; there's a lot more fields in the real struct. Making a tuple like that would span a lot of lines, which would be cumbersome to change in the future.

I don't think I can get away with using references to the fields; I'm building another struct where there corresponding fields (like the keys above) aren't optional, along with some new fields that don't come from the config. The "correct" thing here (to my understanding) is to move the values from the MyAppConfig instance and then dropping the instance.

fluid gorge
low tide
#

I moved what I have working into macros:

macro_rules! take {
    ($struct:expr, $($field_path:ident).+) => {
        $struct.as_mut().map(|x| std::mem::take(&mut x.$($field_path)+))
    };
    ($struct:expr, ($($($field_path:ident).+),+)) => {
        $struct.as_mut().map(|x| ($(std::mem::take(&mut x.$($field_path)+)),+))
    };
}
pub(crate) use take;

macro_rules! take_maybe {
    ($struct:expr, $($field_path:ident).+) => {
        $struct.as_mut().and_then(|x| x.$($field_path)+.take()).take()
    };
}
pub(crate) use take_maybe;

So now I can do:

let mut config = read_config();
let api_key = take!(config, api_key).or_else(ConfigError::NoApiKey)?;
let private_key = take_maybe!(config, private_key).unwrap_or_else(generate_new_api_key);
let (a, b, c) = take!(config, (a, b, c)).unwrap_or_else(|| get_triplet(&foo_bar));
wheat edge
#

If you're taking apart the Config, wouldn't it be better to just destructure the whole thing all at once, then work with the individual parts?

low tide
#

@wheat edge I'm not sure I understand what you mean. If I destructure the Option<MyAppConfig> with something like:

let Some(config) = read_config_file() { } else { }

I will end up writing things like generate_new_private_key twice:

let Some(config) = read_config_file() {
  let private_key = config.private_key.take().unwrap_or_else(generate_new_private_key);
  // ...
} else {
  let private_key = generate_new_private_key();
  // ...
}

That'd be fine until e.g. generate_new_private_key starts needing arguments; now if I want to change the logic, I have to remember to change it in two places

wheat edge
#

Would it be possible to implement a default config? That way you could do

let MyAppConfig { port, api_key, private_key, ... } = read_config_file().unwrap_or_default();

That way you can handle each part without needing to reference through config each time, and without needing to repeat code since you'd always have all of config's fields defined, even if read_config_file() returned None

low tide
#

Yeah I feel like that'd be sensible 😅 I guess the thing there is that I'd like some fields' defaults to be different from their types' defaults (e.g. the port default to be 80 and not 0) but looks like https://crates.io/crates/smart-default can help with that. For some fields I'd like to determine the default value based on e.g. reading stuff from the filesystem but it looks like smart-default might be able to make that less painful too

wheat edge
low tide
#

Oh yeah, I don't mind the deps. Just thought it might be nicer with the crate but I'll check if there's any point to it. Thanks a lot ferrisLove

wheat edge
#

While it's not terrible having the default values in the type def, it can be a bit cluttering to have all those attributes. Personally I think separating them doesn't really detract much while keeping the purposes a bit more clearly separated, and without having hacks like code as string literals as smart default does