Lazy evaluation in pattern matching?

I am writing a top-like utility to profile the memory usage of Linux processes. Here is an excerpt from that code:

match map_path: {
    Path(pathbuf) => {
        let Some(pss) = map.extension.map.get("Pss") else {
            eprintln!("WARNING: PSS field not defined on file-backed map. Assuming 0.\n\
                The map is: {:?}", map);
            continue;
        };
        memory_ext.file_backed_pss += pss;
    },
    Heap => {
        if let Some(pss) = map.extension.map.get("Pss") {
            memory_ext.heap_pss += pss;
        } else {
            eprintln!("WARNING: PSS field not defined on heap. Assuming 0.\n\
                The map is: {:?}", map);
        }
    },
    Stack => { 
        if let Some(pss) = map.extension.map.get("Pss") {
            memory_ext.stack_pss += pss;
        } else {
            eprintln!("WARNING: PSS field not defined on stack. Assuming 0.\n\                                                              
                The map is: {:?}", map);
        }
    },
    Anonymous => {
        if let Some(pss) = map.extension.map.get("Pss") {
            memory_ext.anon_map_pss += pss;
        } else {
            eprintln!("WARNING: PSS field not defined on anonymous map. Assuming 0.\n\                                                      
                The map is: {:?}", map);
        }
    },
    _ => {
        let Some(&rss) = map.extension.map.get("Rss") else {
            eprintln!("WARNING: I don't know how to classify this map and it doesn't have a RSS field.\n\
                The map is: {:?}", map);
            continue;
        };
        if rss == 0 {
            eprintln!("WARNING: I don't know how to classify this map, but at least its RSS is 0.\n\
                The map is: {:?}", map);
        } else {
            panic!("FATAL: I don't know how to classify this map and its RSS is not 0.\n\
                The map is: {:?}", map);
        }
    },
} // end match

So there's a map_path, and independently from that, there's whether "Pss" is in the HashMap. However, if map_path is one of the fallthrough values, then I want to look at the "Rss" key instead.

The action to take in most cases is the same, and the messages are substantially similar. I wonder if I could factor code out, something like:

let pss = map.extension.map.get("Pss");
match (map_path, pss) {
    (Path(pathbuf), Some(pss)) => memory_ext.file_backed_pss += pss,
    (Heap, Some(pss)) => memory_ext.heap_pss += pss,
    (Stack, Some(pss)) => memory_ext.stack_pss += pss,
    (Anonymous, Some(pss)) => memory_ext.anon_map_pss += pss,
    m @ (Path(_) | Heap | Stack | Anonymous, None) => {
                eprintln!("WARNING: PSS field not defined on {}. Assuming 0.\n\
                The map is: {:?}", m.0.to_string(), map); // to_string() hypothetically exists
    },
    (_, _) => {
        let Some(&rss) = map.extension.map.get("Rss") else {
            eprintln!("WARNING: I don't know how to classify this map and it doesn't have a RSS field.\n\
                The map is: {:?}", map);
            continue;
        };
        if rss == 0 {
            eprintln!("WARNING: I don't know how to classify this map, but at least its RSS is 0.\n\
                The map is: {:?}", map);
        } else {
            panic!("FATAL: I don't know how to classify this map and its RSS is not 0.\n\
                The map is: {:?}", map);
        }
    },
} // end match

This code has a lot less repetition. However, maybe I only want to run map.extension.map.get("Pss") if I know I'm not in the fallthrough case for the first tuple item. In other words, I would like to set it up, perhaps with a lambda, so that the match statement only calls the function if it needs to. Is there a way to do this?

you can refactor the similar operations into a closure which captures the common states:

let add_pss_or_warn = |field: &mut i32| {
	if let Some(pss) = map.extension.get("Pss") {
		*field += pss;
	} else {
		eprintln!("warning: {}, {:?}", map_path, map);
	}
};
match map_path {
	Path(path_buf) => add_pss_or_warn(&mut memory_ext.file_backed_pss),
	Heap => add_pss_or_warn(&mut memory_ext.heap_pss),
	Stack => add_pss_or_warn(&mut memory_ext.stack_pss),
	Anonymous => add_pss_or_warn(&mut memory_ext.anon_map_pss),
	Other => {
		let Some(rss) = map.extension.get("Rss") else {
			eprintln!("warning");
			continue;
		};
		// ...
	}
}

note, this will only work if the common variables are read only, so you can capture shared references of them. if they need mutable access, you'll have to tweak the closure, maybe pass them in as arguments.

2 Likes

The only way to add a lazy evaluation in each match arm is to use a guard, but in your case you'd need a if let Some(pss) = map.extension.map.get("Pss"), which is still experimental. You'd have something like this, which remains quite verbose:

match map_path {
    Path(pathbuf) if let Some(pss) = map.extension.map.get("Pss") => memory_ext.file_backed_pss += pss,
    ...

Instead, you could use a simple double match:

match map_path {
    Path(_) | Heap | Stack | Anonymous =>
        match (map_path, map.extension.map.get("Pss")) {
            (Path(pathbuf), Some(pss)) => memory_ext.file_backed_pss += pss,
            (Heap, Some(pss)) => memory_ext.heap_pss += pss,
            (Stack, Some(pss)) => memory_ext.stack_pss += pss,
            (Anonymous, Some(pss)) => memory_ext.anon_map_pss += pss,
            _ => {
                eprintln!("WARNING: PSS field not defined on {}. Assuming 0.\n\
                The map is: {:?}", m.0.to_string(), map); // to_string() hypothetically exists
            }
        },
    _ => { ...

If all the memory_ext fields have the same type, though, @nerditation's solution avoids the double match.

However, if cases that are not Path(_) | Heap | Stack | Anonymous are easy to express, it could simplify the expression by placing that last match arm first (but I suspect it's not the case).

match map_path {
    /* other cases */ => { 
        /* let Some(&rss) = ... */
    }
    _ => match (map_path, map.extension.map.get("Pss")) {
        (Path(pathbuf), Some(pss)) => memory_ext.file_backed_pss += pss,
        (Heap, Some(pss)) => memory_ext.heap_pss += pss,
        (Stack, Some(pss)) => memory_ext.stack_pss += pss,
        (Anonymous, Some(pss)) => memory_ext.anon_map_pss += pss,
        _ => {
            eprintln!("WARNING: PSS field not defined on {}. Assuming 0.\n\
            The map is: {:?}", m.0.to_string(), map); // to_string() hypothetically exists
        }
    }
}
1 Like

Either of these solutions will work for me. But out of curiosity, what would be the issues around adding a feature like this to the language? I.e., something that has the same functionality as

but with syntax similar to

Would it be deemed something that can always be handled with other constructs such as @nerditation's answer? Would it be too rare of a use case?

Can you clarify you question? That syntax that you “propose” already exists and works as expected (at least as expected by me), but what @Unscript wrote does something different.

You want to make compiler automagically call get or what? How would compiler know that it needs to call get and not something else?

The if let ... is adding a feature like this to the language. Making it look like something else is would just be the bike-shedding phase.

Perhaps defining a closure and having match call it lazily in the scrutinee:

let pss = || map.extension.map.get("Pss");
match (map_path, lazy pss) {
    (Path(pathbuf), Some(pss)) => memory_ext.file_backed_pss += pss,
    ...

That's certainly possible but from my limited experience with Haskell… I'm not sure I would want that.

Properly coding with lazy evaluation is not easy. Or, rather, some things are becoming easier, but some other things become harder. And if you just only keep it for pattern matching it looks like incredibly limited feature not worth the complexity.

Maybe something close to what you want would be defining a closure that returns pss and use it in guards. In this example, I'm not sure it would be much shorter, though. You also have to consider maintenance, if someone else has to read and understand the code later.

So something like this, once that feature is stabilized:

let get_pss = || map.extension.map.get("Pss");
match map_path {
    Path(pathbuf) if let Some(pss) = get_pss() => memory_ext.file_backed_pss += pss,
    Heap if let Some(pss) = get_pss() => memory_ext.heap_pss += pss,
    Stack if let Some(pss) = get_pss() => memory_ext.stack_pss += pss,
// etc...

Modifying the language to express something relatively rare in a slightly shorter way doesn't seem like a good long-term evolution strategy.

If you're using that specific pattern all the time, you may consider using a declarative macro that transforms a short syntax into the more lengthy code required by the compiler. :slight_smile:

1 Like