Hi there, before opening an RFC I thought I solicit some pre-feedback here.
For some time I've missed having a particular fundamental primitive function that sees frequent use in Haskell, void:
void :: Functor f => f a -> f ()
In other words, "ignore the contents of your container/context", which is especially useful for chaining operations together.
Hey now, wait a minute, we already have something in Rust for "ignoring results": the ;
"operator". We use this all the time in conjunction with ?
to write what a Haskeller would see as Monadic code in the only two "monads" that really matter in life, Result
and Option
. So we often see lines line:
fn foo() -> Result<(), Error> {
let a = bar()?;
let b = baz()?;
work(a, b)?;
Ok(())
}
Assuming some composite Error
type, and that each ?
'd line is actually returning a different underlying error.
So from day-1 of my Rust journey, Ok(())
struck me as code smell. Indeed Haskell linters will warn you if you end a function in the equivalent pure ()
, citing that void
should be called instead on the previous line.
Further, for some strange reason, we're unable to write:
fn foo() -> Result<(), Error> {
let a = bar()?;
let b = baz()?;
work(a, b)?
}
The last line will error. And if work
returns an actual value inside Result
(as Command::status
, etc, do!), we can't "drop it to ()
" using ;
in the usual way, since ;
forces the entire line to be ()
(i.e. it doesn't respect the "context"). Thus we must write an extra Ok(())
below that or do the verbose:
work(a, b).map_err(|e| ... manual lifting here! ...).map(|_| ())
If we're not working with a composite error type, then a last line of work(a, b)
works fine, but that's not an option here.
To summarize, the issues are:
- If the final call returns a non-
()
value, but we want()
, we must write an extraOk(())
. - If we want to lift to a composite error type, we must use
?;
, and thus an extraOk(())
below it. - We often want to do both (1) and (2) at the same time.
Hence as a reduction of boilerplate, I propose the following method be added to Result
:
pub fn void<R: From<E>)(self) -> Result<(), R> {
match self {
Ok(_) => Ok(()),
Err(e) => Err(From::from(e)),
}
}
This void
kills two birds with one stone; it ignores the result value and lifts our error. This enables:
fn foo() -> Result<(), Error> {
let a = bar()?;
let b = baz()?;
work(a, b).void()
}
Here are some examples in real code:
/// Search the Pacman log for a matching string.
pub(crate) fn search(path: &Path, term: String) -> Result<(), Error> {
let (search, args) = misc::searcher();
Command::new(search)
.args(args)
.arg(term)
.arg(path)
.status()
.void()
}
/// Open a given URL in a browser.
pub(crate) fn open(url: &str) -> Result<(), std::io::Error> {
webbrowser::open(url).void()
}
^ Here we don't lift the error, we just want ()
. void
turns it into a one-liner, which probably helps inlining?
let clones: Validated<(), &str> = packages
.par_iter()
.map(|p| {
let pkg = p.as_str();
aura!(fll, "A-w", package = pkg);
clone_aur_repo(None, &p).ok_or(pkg).void()
})
.collect();
^ Here we're using void
in a lambda to drop to ()
, so that Validated
can smash them all together via collect
.
The void
in the code above is implemented as a trait
:
pub(crate) trait ResultVoid<E, R> {
fn void(self) -> Result<(), R>
where
R: From<E>;
}
impl<T, E, R> ResultVoid<E, R> for Result<T, E> {
fn void(self) -> Result<(), R>
where
R: From<E>,
{
match self {
Ok(_) => Ok(()),
Err(e) => Err(From::from(e)),
}
}
}
But this forces an extra import statement, and void
should really be part of Result
directly (or Rust should get HKTs and Functors). And while less useful since there's no Error lifting, a similar method could be added to Option
for the convenience of dropping to ()
when we need to.
Now I'm not by any means attempting "import Haskell" into Rust just for the sake of it, but void
as I've described here seems to me to be a clean solution to the outlined problems.
Thanks for any input you can offer.