So, I've been wanting to start with blogging for a while now. I always stopped short of writing a single word due to exhaustion, feeling like I had to find something more interesting, that it wasn't "the right first blog post"... et cetera.
Anyway, today I found myself in the mood to mess around with Rust again; specifically, new fun ways to write cursed syntax. Eventually, I landed on the ? operator, and the experimental try_trait_v2. In brief, this is a set of traits, which allow implementors to make use of the ? operator for more convenient error checking. Without further ado, here's a snippet of the cursed code that I came up with:
Edit: I managed to refine the code a little - it no longer needs a try { } block, and has some other improvements. See Edit 1.
// [Implementation code above...]
const S: TryPrinter = TryPrinter(AsciiChar::Space);
fn main() {
TryPrinter(_) = try {
S????????????????????????????????????????;
S?????????????????????????????????????;
S????????????????????????????????????????????;
S????????????????????????????????????????????;
S???????????????????????????????????????????????;
S????????????;
S;
S???????????????????????????????????????????????????????;
S???????????????????????????????????????????????;
S??????????????????????????????????????????????????;
S????????????????????????????????????????????;
S????????????????????????????????????;
S???????????????????????????????
};
}
You may try to guess what this code does... Really, it's nothing too complicated, and there are a lot of hints everywhere anyway.
Show
It's a simple Hello World - or, rather,HELLO, WORLD?
Here are the implementation details:
Firstly, there's try_trait_v2. This unlocks the Try and FromResidual traits, allowing you to implement them on your types to make them usable with the ? operator.
#![feature(try_trait_v2)]
#![feature(ascii_char)]
use std::ascii::Char as AsciiChar;
use std::ops::Try;
struct TryPrinter(AsciiChar);
impl FromResidual for TryPrinter {
// ...
}
impl Try for TryPrinter {
// ...
}
? is Rust's early-return operator, often a convenient shorthand for the equivalent of a null check, but it also propagates errors from Result<T, E>. While you may think of them as a simple match on the option or result with a return in the None or Err cases, the actual logic is a bit more complex. Importantly, the value is sent through Try::branch(self) first, which decides whether to continue, or break out early.
impl Try for TryPrinter {
type Output = TryPrinter;
type Residual = ();
// ...
fn branch(mut self) -> ControlFlow<Self::Residual, Self::Output> {
let c = self.0;
match AsciiChar::from_u8(c.to_u8() + 1) {
None => ControlFlow::Break(()),
Some(x) => {
self.0 = x;
ControlFlow::Continue(self)
}
}
}
}
Try is defined in terms of two associated types: Output and Residual. Commonly, Output is the equivalent of a successful unwrap, and Residual is like a failed null check or a propagated error - however, your implementation may do anything else instead. My TryPrinter type simply stores an ascii::Char (a newtyped u8), and the Try implementation on it does one of two things: it returns the TryPrinter with the character incremented, but if it would overflow, it instead returns a Residual via ControlFlow::Break (in this case, I just made it return ()).
Edit 1: I split TryPrinter into a generic IterOnTry and a (non-generic) AsciiCharDropPrinter. See below for details.
Now, chaining ? operators is completely valid syntax already. Weird, maybe, but Option<Option<T>> can absolutely accept ?? as a double-unwrap1. My attempt to abuse this led to a type whose Try::Output was simply itself, which allowed for infinite ? chaining. It's no fun when it's all no-ops, so I instead made it store a value, which gets incremented by every subsequent ?.
The second part of the secret sauce is my favorite trick, print! on drop. Each line in the sample code starts with the S constant2, increments its char value repeatedly, and drops the struct at the end of the statement, printing the stored character to the screen. Edit 1: again, the types have been shuffled around slightly, see below for details.
impl Drop for TryPrinter {
fn drop(&mut self) {
print!("{}", self.0);
}
}
Finally, some extra fluff around the code is needed to make it all work:
- Implementors of
Tryalso need to implementFromResidual- I just made it return something and didn't bother with it all that much3.- Edit 1: the Residual type is now
Option<Infallible>
- Edit 1: the Residual type is now
?may only be used in functions that return anOptionor aResult. Rather than making a wrapper function, I used the (also experimental)try { }block.- The
try { }block needs help with type inference in this case, so the block is assigned to a pattern (and the last expression has no semicolon). - Edit 1:
try { }is no longer needed
- The
- I used the
Sconstant so I could start with space (0x20) rather than NUL (0x00), because I wanted the question marks to fit on the screen.
Check out the Playground link for the full source code. Edit 1: Playground link v2
One last note: I'll see if I can get rid of the try { } block by implementing std::process::Termination somehow to let me use ? within main itself. Something about the type inference requirement also doesn't quite feel right, and might be related to my messy implementation of FromResidual, but I'll need to understand the traits better to solve that. See Edit 1.
I've been wanting to start with blogging for a while now. I always stopped short of writing a single word for various reasons, but I guess the only way to get it done is to simply try. I love messing around with Rust in all sorts of ways, and this experiment lent itself well to a nice little blog writeup.
And what better way to start with blogging than with a HELLO, WORLD?
Edit 1
I thought about it further and made some tweaks to the implementation:
I split TryPrinter into a generic IterOnTry<T>, and a specific AsciiCharDropPrinter. IterOnTry<T> is now responsible for the ? part of the mess - it holds an Option-wrapped Iterator, and calls next() on it each time ? is applied. If the iterator runs out, it stores a None instead. AsciiCharDropPrinter is now solely responsible for incrementing its ascii::Char, and implementing Drop-printing - generic types are not allowed to implement Drop due to some unsoundness issues that could cause.
This made the whole implementation more generic and extensible.
Residual is Option<Infallible> now instead, which also makes it compatible with Option-returning functions (due to Option<T>: FromResidual<Option<Infallible>>). By extension, IterOnTry<T> : FromResidual<Option<Infallible>> as well, which simply yields an IterOnTry(None) - pretty much the same as the Option<T> impl.
This made the Residual type more proper/meaningful, as well as compatible with existing implementations.
Finally, by implementing std::process::Termination on IterOnTry<T>, ? can be used directly in main() (assuming main() returns some sort of IterOnTry<_>).
This removes the need for a try { } wrapper (and related type inference issues).
I also cleaned up some redundant impls, dropped ! in favor of Infallible (one less feature gate, compat with existing impls), and added some comments. More importantly, though, I think I have a better understanding of the traits now as well!
Footnotes
-
Though, you might want to use
Option::flatten(self)instead; ditto forResult::flatten(self)once it gets stabilized. ↩ -
constitems may implementDrop! Every time you refer to the constant, a new instance is constructed at that location, and that gets dropped as usual. Theconstitem itself, though, doesn't get dropped aftermainor anything like that. ↩ -
My rough understanding is:
Try::Residualis a "definitely-error" version of the main type, andFromResidualre-pairs it with an output type again. This means you can go fromResult<T, E>throughResult<Infallible, E>toResult<U, E>without needingTandUto be the same type. In any case, given how I implemented myTryPrinterstruct here, I didn't bother with figuring out a proper error/residual type. Note: see also Edit 1 ↩