
Implementing a Type-safe printf in Rust - lukastyrychtr
https://willcrichton.net/notes/type-safe-printf/
======
RcouF1uZ4gsC
Note that this is a similar technique to one done in Andrei Alexandrescu's
book Modern C++ Design that used C++98.

IIRC he called it TypeList and used that to actually implement tuple as
library type.

When C++11 variadic templates came out, a lot of C++ meta programmers breathed
a huge sigh of relief of being able to abandon that technique. In addition, to
being a pain to work with, because of the extensive use of recursion, it can
take a large toll on compile times.

Variadic templates and being able manipulate tuples generically are probably
the two biggest things I miss when programming Rust.

~~~
pjmlp
And with constexpr and its variants, alongside auto templates, fold
expressions, and now concepts, yet another legion breathed a huge sigh of
relief to be able to abandon all those tag dispatch based techniques.

------
WalterBright
D simply checks the arguments to printf against the format string. This
facility exists as an extension in most C and C++ compilers.

D's writefln() function uses variadic template specializations to achieve a
similar result, meaning you can just use `%s` for the formats and writefln()
knows how to print each type.

~~~
quietbritishjim
Are you saying the compiler itself does the checking? That is a pity. Compiler
magic for a checking a fixed specific format system (I don't know about D, but
that's what you're describing in C printf) is significantly worse the compile-
time checking being written in the language itself.

What if you, as library author (not compiler writer!), think of a much better
formatting system? Or something that's maybe not better in general, but better
for an unusual situation you're deploying in? Or you want to write something
that's different to a formatting system but uses similar tricks? If you don't
have language facilities to do this yourself then you're stuffed.

As well the original post showing it's possible in Rust, this is also possible
in C++. For example, the C++ fmt [1] library allows compile-time time checking
of the number of slots in format strings against the number of arguments, and
even that the format specifications (.03d or whatever) match the passed in
types. And it's not reliant on the compiler doing its own magic checking
outside of the language.

[1] [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt)

~~~
MaxBarraclough
_edit_ I see I'm too late, Walter already answered this. I'll leave this here
anyway.

I don't think D's _writefln_ is getting any special treatment from the
compiler. It seems to use D's compile-time features to perform the check.

[https://github.com/dlang/phobos/blob/v2.093.0/std/stdio.d#L4...](https://github.com/dlang/phobos/blob/v2.093.0/std/stdio.d#L4095)

~~~
AnIdiotOnTheNet
Zig does it in a similar way, using the compile time type information
available to all Zig code, and is in fact implemented in the std library. No
magic necessary.

~~~
renox
But both fail short to Python/bash.. format strings where you can have "in
place" reference to variable which IMHO is much more readable.

~~~
ben-schaaf
I completely agree that string interpolation is often superior to a format
string, but it's important to note that they are not equivalent. String
interpolation is generally evaluated in-situ and has full access to the local
scope. Format strings on the other hand can be passed around and manipulated
(even at compile-time) and can only ever access the data they're formatted
with. strftime and strptime are a good example of a format string usage that
can't be replaced by string interpolation in python.

D also has the capability of implementing string interpolation using mixins -
though the resulting code isn't quite as neat.

------
hyperman1
One thing i really miss from C's printf or Java's MessageFormat is the ability
to change argument positions: Different languages can display the argument in
different orders, and can have the format string in a resource bundle.

It is of course very hard to have type safety when the format string is in a
resource bundle

~~~
dthul
The position of arguments is actually not required to follow the format
string. From the documentation:

    
    
      println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2"
    

But you are right, you cannot dynamically change the format string. If you
need this facility, there is the runtime_fmt crate which will parse the format
string at runtime instead of compile time. That of course means that it can
fail at runtime if the format string does not match the arguments.

~~~
amelius
Why use numbers and not names, though?

~~~
dthul
You can!

    
    
      println!("{a} {c} {b}", a="a", b='b', c=3);  // => "a 3 b"

------
skohan
One feature I really miss in Rust is being able to insert variables in place
in strings, like you have in Swift, or JS template literals.

~~~
stjepang
This is being worked on: [https://github.com/rust-
lang/rust/issues/67984](https://github.com/rust-lang/rust/issues/67984)

~~~
skohan
Neat. It's interesting that they've chosen just plain braces to denote the
argument insertion; i.e: "text {value}" rather than something like "text
${value}" which seems to be what most languages use. I guess there must be a
way to escape it if you just want braces in your string.

~~~
stjepang
Right, there is a way - to escape braces and print "text {value}" rather than
the actual value, you'd use "text {{value}}"

------
lmm
It'd be worth reusing the HList (and the macro for instantiating it) from
frunk rather than making a custom HList type.

(This is how the Scala ecosystem ended up working with records; technically
the language doesn't have proper record types, but the whole ecosystem uses
Shapeless to the extent that IDEs and other tooling are expected to understand
the Shapeless macro, so de facto it does)

~~~
kelnos
Pretty sure that's what the author was doing. The quick HList implementation
shown in the post was just to explain what HLists are; the author later
mentions (and presumably uses) frunk.

------
dependenttypes
Here is one in Idris.
[https://gist.github.com/chrisdone/672efcd784528b7d0b7e17ad9c...](https://gist.github.com/chrisdone/672efcd784528b7d0b7e17ad9c115292)

Because Idris supports dependent types it is prettier both in the
implementation and usage.

------
signa11
imho, with c++'s variadic templates doing the equivalent there is quite
trivial.

~~~
dan-robertson
The issue with templates is that you can’t really type check them at the point
of definition. Instead they need to be checked each time the template is used.

This can make compilation slow but it can also make template writing very hard
because a bug might only appear when a template is instantiated with certain
types.

~~~
quietbritishjim
The parent comment's point is just that variadic templates would make it
easier to do the exact job talked about in this blog post. It sounds like
you're objecting but you haven't quite said that outright. Are Rust macros
less susceptible to the problem you're describing?

~~~
monadic2
Yea, traits make this much simpler to avoid. The problematic type would not
match the trait, likely leading to much simpler communication of the error.

~~~
quietbritishjim
I'm not really convinced. In C++, you could start your hypothetical format
function with static_assert(formatter<T>:: exists) or similar and get a very
similar error message. (In C++ template terminology, a class like that is also
called a trait, funnily enough, but that's just a convention rather than a
language feature.) I think you could use C++ concepts to check add part of the
function signature, but the functional difference would be very small. And
none of these – including Rust traits – help you with compile time checking of
format specifiers.

------
jganetsk
They should have used existential types instead of having so many type
parameters. I rewrote it with existential types and it was cleaner. This is
probably due to the author importing an example from Haskell, language where
existential types aren't as prevalent.

------
The_rationalist
It's still a shame that rust doesn't support variadic functions and hence the
hacky need of implementing printf through a macro.

~~~
methyl
What are the downsides of current printf macro implementation vs a variadic fn
one?

~~~
lmm
\- Possibility of implementation errors

\- A reader can't really reason about what a macro might be doing, since
they're immensely powerful.

\- Tooling can't necessarily understand macros correctly, for the same reason.
E.g. automated refactoring like extracting a repeated expression can't be
relied on to work correctly.

~~~
methyl
These are downsides of macros in general, but I don't see how they are
relevant to a built-in macro from stdlib.

~~~
lmm
They apply just the same in the standard lib surely? I suppose tooling might
be expected to have support for the standard lib, but you're creating a lot
more work for tool implementors if there is a large (and presumably evolving)
set of "standard" macros. The concern about the reader is definitely still
there unless you're expecting every Rust user to memorize which macros are in
the standard library.

------
hardwaresofton
tl;dr rust is cool, type level programming excites haskellers, you should take
rust for a spin because it somehow manages to seemingly do it all.

Note that this is the kind of type machinery that Haskellers get excited
about. If you're somewhat familiar about what type level programming in this
area looks like there, check out Hlist[0], Vinyl[1][2], and if you're really
interested, phantom types[3].

One of the best things about rust is that it manages to have both a near-
research-grade type system, approaching C speed, and novel data safety
features all rolled in one language. If those features weren't enough, it's
got an extremely good module system with public package hosting, helpful
compiler, relatively easy cross compilation and toolchain convenience other
languages only dream of along with a welcoming community.

I'm not associated with the rust team/mozilla in any way, but there are very
few reasons to not be excited about where rust will be in 10 years. Whether
you're writing code to run in browsers (compiling your frontend code to wasm
is a pretty intense step I'll admit -- I like my TS/JS just fine), web
servers, or embedded code to run on your small ESP32 (or smaller), rust
somehow fits.

[0]:
[https://hackage.haskell.org/package/HList](https://hackage.haskell.org/package/HList)

[1]:
[https://hackage.haskell.org/package/vinyl-0.13.0/docs/Data-V...](https://hackage.haskell.org/package/vinyl-0.13.0/docs/Data-
Vinyl-Tutorial-Overview.html)

[2]:
[https://github.com/VinylRecords/Vinyl/blob/master/tests/Intr...](https://github.com/VinylRecords/Vinyl/blob/master/tests/Intro.lhs)

[3]:
[https://wiki.haskell.org/Phantom_type](https://wiki.haskell.org/Phantom_type)

~~~
akiselev
_> I'm not associated with the rust team/mozilla in any way_

Is there still a Rust team at Mozilla?

~~~
hardwaresofton
This came up on r/rust:
[https://twitter.com/rustlang/status/1294024734804508679](https://twitter.com/rustlang/status/1294024734804508679)
(Reddit thread:
[https://www.reddit.com/r/rust/comments/i994km/core_team_stat...](https://www.reddit.com/r/rust/comments/i994km/core_team_statement_so_far_we_are_sad_for_our/))

