Flatten as a pseudo-generator
Has functional programming gone too far?
This is a brief discussion of a snippet of code from a more junior member of my team that perplexed me when I first read it. I’m sharing it in part because it was a neat trick and also because it exposed me to a weird, honestly unnatural aspect of rust.
The Code
This is modified for brevity and because the original code is proprietary. The context here is that we have a series a different servers that our service can start up. Some of them only exist on Windows, others only on Linux. Some are always enabled, others are conditionally enabled based on config state. If any server exits, we need all the others to exit as well. This is accomplished by code that accepts a collection of server exit futures, so we need to group them all together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let servers = [
Some(start_foo(
config_fetcher.clone(),
)),
Some(start_bar(
&environment_context,
config_fetcher.clone(),
)),
#[cfg(windows)]
config
.windows_only_thing
.enabled
.then(|| start_windows_only_thing(config_fetcher.clone())),
#[cfg(unix)]
config
.linux_only_thing
.enabled
.then(start_linux_only_thing(config_fetcher.clone())),
]
.into_iter()
.flatten();
So then, what’s the type of servers
?
Going one layer at a time, the usage of Option
is pretty straightforward, it’s to that then
can be used rather than say a mutable value with a conditional if
wrapping a push
call to a Vec
.
But what on earth is flatten
doing here?
Creates an iterator that flattens nested structure.
This is useful when you have an iterator of iterators or an iterator of things that can be turned into iterators and you want to remove one level of indirection.
Examples
Basic usage:
1 2 3 let data = vec![vec![1, 2, 3, 4], vec![5, 6]]; let flattened = data.into_iter().flatten().collect::<Vec<u8>>(); assert_eq!(flattened, &[1, 2, 3, 4, 5, 6]);
Option
Implements Iterator
And this is what threw me for a loop. Evidently, Option
implements Iterator
. Per iterating over option, “this can be helpful if you need an iterator that is conditionally empty.”
That makes sense, and whether intentional or not that means when you have an iterator of options, flatten
is equivalent to doing a filter_map
where the filter portion is only Some
values and the map portion is unwrapping into the inner value.
Alternative
Personally, I don’t think this is a good application though. It makes for a bit less typing, but it’s a strange application that is challenging for the read to follow on the first pass. We don’t want to find ourselves of the cult of conciseness. White space and keywords are our friends. Generally speaking, iterators should be used to create processing pipelines and when a function (such as filter
, any
, or all
) is more expressive than its corresponding code is.
Here, we’re not increasing expressiveness. So this would be better:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let mut servers = vec![
start_foo(
config_fetcher.clone(),
),
start_bar(
&environment_context,
config_fetcher.clone(),
),
];
#[cfg(windows)]
if config.windows_only_thing.enabled {
servers.push(start_windows_only_thing(config_fetcher.clone()));
}
#[cfg(unix)]
if config.windows_only_thing.enabled {
servers.push(start_linux_only_thing(config_fetcher.clone()));
}
let servers = servers.into_iter().flatten();