Generics and Compile-Time in Rust
pingcap.com
pingcap.com
But there is an area that could have a big impact on certain (mostly higher level) domains, yet doesn't seem to get much attention: better trait objects.
They are severely limited in a few aspects:
* only a single trait/vtable
* casting is only available with Any and you can't cast between different traits, requiring really awkward super-traits with manual conversion methods or hacks like mopa [1]
* object safety rules are cumbersome and prevent certain important traits like Clone to be available, leading to clone_boxed, clone_arc everywhere, or proc macro solutions like dyn-clone [2]
* ...
Doing anything more fancy with them usually feels annoying. Therefore the standard library and entire ecosystem strongly favor generics and monomorphization.
This is generally fine and has worked out well for the language, but there are plenty of use cases where more advanced trait objects could reduce code size and compile times with very small impact on performance, while also enabling some interesting new patterns.
I realize there are plenty of implementation challenges that make work in this area far from trivial in the current language, but it's frustrating to miss out on part of a toolbox.
I think Swift is an interesting comparsion. The languages are similar in quite a few aspects, but Swift often prioritizes small code size and dynamic dispatch over monomorphization. Compile times aren't that great either, though...
ps: it is briefly mentioned in the post, but switching to LLD has provided noticeable build time improvements on most of the binary crates I am working on.
[1] https://github.com/chris-morgan/mopa [2] https://github.com/dtolnay/dyn-clone
I'm in disagreement with the parent, dynamic dispatch through vtables is only a zero-cost abstraction across FFI boundaries. Personally I'd ballpark about 33% of the value-add of traits are shared interfaces on different types.
The real money is in associated types and trait bounds. The latter is still a serious type-checking requirement at compile time, and the former requires RTTI to support dynamically in some form - both of which have costs at compile and runtime.
All that said, there may be an argument that the features GP is talking about are covered currently by enum variants (many of the use cases one would have for base classes with associated member variables are done that way, for example), and if you could show how performance and ergonomics improved by increasing the semantic complexity of dyn trait objects - you'd have a strong argument to add it to the language. Just my two cents.
[0] https://github.com/microsoft/com-rs/
[1] https://github.com/RustAudio/vst3-sys
[2] https://github.com/RustAudio/vst3-sys/blob/master/examples/p...
To elaborate a bit on the part about new patterns, I've encountered issues where trait objects allow me to define APIs that otherwise wouldn't be possible. One example is when developing something like a database driver, you might define a trait EventHandler with the methods `handle_start_event` and `handle_completion_event`, where users can pass in values of types that implement EventHandler, and you call their handling methods whenever you start or complete an operation. The most straightforward way to specify this would be to have your client type have an internal vector of EventHandlers that you can iterate over and call the corresponding event handling methods whenever needed. If you use generics for this, then your client type will need to be generic over the type of EventHandler it can contain, which means you can't specify EventHandlers with different concrete types. The best way to get around this is to use something like `Vec<Box<dyn EventHandler>>`.
I understand the sentiment behind favoring generics over trait objects in the Rust ecosystem, as strongly preferring compile time costs to runtime costs when there's a choice between them is one of the more fundamental guiding principles to the Rust ecosystem (and is one of the things I really like about Rust), but there are patterns that trait objects allow that just can't be expressed with generics, and when you need to use one of them, it can be frustrated to hit some of those sharp edges you mentioned.
https://lukepalmer.wordpress.com/2010/01/24/haskell-antipatt...
struct EventHandler { handle_start_event: fn(), handle_completion_event: fn(), }
...but this differs in a key way from the haskell example: Rust's `fn` is not a closure, it's just a pointer to some code, and you can't construct them dynamically. If you want to pass around a _closure_, you need ...a trait object.
Something like:
struct EventHandler { handle_start_event: Box<dyn impl Fn()>, handle_start_event: Box<dyn impl Fn()>, }
...but that's exactly the pattern the author of that post is suggesting getting away from.
I agree with the author's point, but it doesn't really apply to Rust because there isn't actually a simpler way to do it. And I think that's part of what the OP was getting at -- Trait objects are the closest thing Rust has to proper closures, and they're a bit awkward.
Note that I'm still something of a newbie at Rust, and don't feel like I can really say much about the day to day experience of using (or avoiding) trait objects due to limited experience.
Also, there's a lot of disagreement about exactly what the distinctions between `lambda`, `closure`, `anonymous function` and friends mean. The interpretation of `closure` I'm interested in is basically a bundle of code and data, which can be treated abstractly in a first class way. That could be something that appears as a lambda capturing variables in the text of your program or as an object in a more OO setting which, albeit more verbose for a single function, has a similar level of power of abstraction.
Either way, part of that power is being able to write code that can work with the interface (irrespective of the shape of the enclosed data), and do things like store differing implementations in lists/vecs etc. The only mechanism Rust provides to do that is trait objects.
I don't understand the issue (and I know very little of rust), but isn't `dyn impl Fn(A) -> B` exactly that type? Any language with first class function parameters and objects, but type erases them (unlike rust and c++) would implement them pretty much like the dyn impl above under the hood.
But dyn impl is a trait wrapped in an existential. There's no concrete type a -> b, only a trait for types that can be 'called.' So to pass an arbitrary function around in a record/struct you need to do the very thing the post is saying is an antipattern (in Haskell). So the argument doesn't really apply to Rust because there isn't a simpler more direct way to achieve the same thing.
Instead of the Vec holding concrete instances of EventHandler, it holds pointers to them (because of the Box). And then because of the dyn, the compiler can't generate static-dispatch code when you later inevitably want to call functions on the EventHandler, so those function calls have to be resolved at runtime.
With some metaclass / reflection magic maybe one could come up with an additional scheme that would save the per-object vtable pointer through creation of a "duplicate" structure - e.g. given
struct Event {
virtual ~Event();
virtual bool whateverAPI();
pair<double,double> globalPosition();
pair<double,double> localPosition();
private:
double x{}, y{};
Widget& widget;
};
struct MyEvent : Event {
// or whatever, just an example
bool whateverAPI() override;
private:
MouseButtons button{};
};
one would get a sub-array of things with a layout similar as struct synthesized_MyEvent {
double x{}, y{};
Widget& widget;
MouseButtons button{};
pair<double,double> globalPosition() { /* potentially UB magic */ }
pair<double,double> localPosition() { /* potentially UB magic */ }
bool whateverAPI() { /* potentially UB magic */ }
};
("potentially UB" being something like copying all the non-vtable things into a stack-allocated concrete MyEvent and calling the virtual function on that which could be worth it for small objects in terms of cache usage if the vtable is 8 bytes and the custom data something like 16 bytes and there's 350000 objects)That's exactly the right solution to your problem. It's not a workaround. It is what you need.
What you need is a heterogenous collection of objects that implement a common interface. And the exact type isn't available to you because it's provided by the client. So you need to go through virtual (or dynamic) dispatch.
Just imagine how you might write this in C++: you just define an EventHandler abstract class and ask that clients inherit from it. Then you take pointers to EventHandler and store them.
Or imagine how you might write it in Haskell: simple existential types. The type class dicts are stored by the compiler into your data type to enable dynamic dispatching.
This is object-oriented polymorphism 101. I think Rust's anti-OO pendulum has swung so far that people can't even see that what they need is basic OO.
But I understand completely the mindset. If you think that you can implement your solution with full static dispatching and no pointer indirection, having to add these "ugly workarounds" feels like surrendering.
But there is a continuum between full static dispatch and boxing and indirection everywhere. Adding a few carefully chosen "dynamic joints" can add a lot of runtime flexibility at a minimal cost.
Yep, I agree! To clarify, the issue for me isn't having to use trait objects like this, it's the ergonomic issues that my parent comment brought up, e.g. not being able to require `EventHandler: Clone`. I'm not opposed to trait objects themselves; I just wish they were easier to use.
fn print<T: ToString>(v: T) {
> We say that “print is generic over type T, where T implements Stringify”Show is a very common but generic word in programming, for showing plots, showing message boxes, showing anything really. Not knowing Haskell I would not have expected it to convert an object to a string.
On the doc of ToString you can find the following note:
>This trait is automatically implemented for any type which implements the Display trait. As such, ToString shouldn't be implemented directly: Display should be implemented instead, and you get the ToString implementation for free.
I think ToString exists mostly in order to have a more convenient API to get a String out of a display-able type, otherwise if you used Display directly you'd have to use a variant of that boilerplate every time: https://doc.rust-lang.org/src/alloc/string.rs.html#2185-2192
If you were actually reading it aloud, I would strongly avoid that use of the word “where”, because it’ll be confused with a where clause:
fn print<T>(v: T) where T: ToString
The two are semantically identical, but they put the constraint in different places, so using the word “where” will confuse things, especially when it leads to you repeating the “T” in a way that where clauses do but the first form doesn’t.For myself, I would probably read the original line of code as “function print, generic over type tee implementing to string, taking parameter vee of type tee”.
The where clause version I’d read as “function print, generic over type tee, taking parameter vee of type tee, where tee implements to string”.
https://abissell.com/2014/01/16/c11s-_generic-keyword-macro-...
Huh, TIL. Branch prediction is normally about predicting which branch an `if` would take. But apparently this applies to indirect jumps as well: https://stackoverflow.com/a/26240197/1082652
> The compile times we see for TiKV aren't so terrible, and are comparable to C++
So if you're already used to the terrible compile times of C++, the compile times of Rust won't seem that bad in comparison. And Mozilla, where Rust started, mostly relies on C++. That does explain a lot...
Also a problem that for 95% of code written speed doesn't matter at all.
Combine those two and programmers a paying a lot for something that isn't needed. AKA 99% of the time you're compiling a module that hasn't changed. And 95% of the code in those modules speed doesn't matter at all.
> 1. translate the generic function for each set of instantiated type parameters
> 2. translate the generic function just once, calling each trait method through a function pointer (via a “vtable”).
The approach in haskell might be considered a variation of 2, since it involves indirection, but a little different from other languages normally using vtables, since it's not selecting different implementations at run time, but just looking up the pre-determined implementation through a new parameter.
In particular, the function is transformed into a higher-order function accepting a new parameter representing the dynamic to_string functinoality, then at the call site, the appropriate concrete implementation to_string parameter is inserted for the new transformed function, and similarly this new higher-order function 'print' only needs to be compiled once.
Haskell isn't 0-cost-abstraction like Rust ofc, but it is definitely a minimize-the-cost-of-abstraction. And the control over said minimization is getting better with each release.
Higher rank types also break this, as you can define things like:
newtype GenericThing = GenericThing (forall a. [a] -> a)
doManyGenericThings :: [GenericThing] -> [a] -> [a]
doManyGenericThings things list = map (\GenericThing f -> f list) things
Here, `doManyGenericThings` takes a list of generic functions, and applies each of them to its argument. You can't just monomorphize it, because you'd have to somehow also monomorphize every argument it is ever passed, which is a dynamic property that you can't know in advance (and again, may not be a finite set).
It's in fact the same.
(This is basically why Rust doesn't support multi-trait types (e.g. &dyn Debug + Clone)
trait Foo: Debug + Clone {}
Edit: No you can't, because `Clone` requires `Sized`, which means you can't make a trait object of `Foo`.https://play.rust-lang.org/?version=stable&mode=debug&editio...
Edit: Assuming we're talking about objects that can be grouped into some sort of 'classes'.
Perhaps gpderetta said it better: https://news.ycombinator.com/item?id=23537639
The advantage is that "interfaces" can be attached to objects non intrusively (and outside of that object declaration, which is a huge win for interoperability), the disadvantage is that pointers are larger and there is no easy way to cross cast between interfaces.
In c++ "fat pointers" (also sometimes known are virtual concepts) are often implemented manually, the most well known example is std::function. We are all waiting for compile time reflection to be able to implement virtual concepts generically (you can only approximate it up to a point currently).
Obj f(Obj a, Obj b);
in C++, you pass in Obj's vtable twice, and the returned value includes a vtable.If you have
f :: Obj a => a -> a -> a
in Haskell, you pass in one dictionary. If you have f :: (Obj a, Obj b) => a -> b -> a
in Haskell, you pass in two dictionaries. If you have f :: (Obj a, Obj b, Obj c) => a -> b -> c
you pass in three dictionaries, and the caller gets to decide what the concrete type of c is, NOT the function implementation. data DynObj = forall c. Obj c => DynObj c
f :: (Obj a, Obj b) => a -> b -> DynObj
In this case, you pass in two dictionaries and the returned value has a vtable. This essentially is what C++ does.This is really, really powerful.
Even with unsafe C++, I’m used to writing very high level, high performance concurrent code. If it crashes after it’s passed CI testing for a week or so, it’s probably a hardware bug.
(Of course, there are exceptions, but those fall outside of the parts I have the compiler check for me.)
An example: this function is only valid on positive, odd integers in the range 3-91. With any static type system, this has to be a runtime check anyway.
I’m not an expert on dependent types, but that sounds like what those try and solve. But at that point, you just want compile time to be a full program, which has all of the same pitfalls as the runtime of a program.
One really nice use of this is printf. In C, printf needs a remarkably large amount of code and it’s quite slow. In zig, printf marks the format string as comptime. Then the compiler runs the code looping over and evaluating the format string, then unrolls the result into the binary. The output ends up being a few terse calls to write / format with no extra machinery at runtime. And you get clean compile errors if the format string doesn’t match your arguments - all with no special casing in the compiler.
More detail, with code: https://ziglang.org/documentation/master/#Case-Study-printf-...
I think nim has something similar.
I’m surprised rust went with macros instead of something like comptime - though I assume there’s some trade offs I’m not aware of.
Rust has procedural macros which are Rust libraries built and run compile time that can transform raw tokens into valid Rust code (or more raw tokens for nested macros). Procedural macros can annotate any statement or code block - although some specific ones might still be on nightly only.
[0] https://github.com/rust-lang/rust/issues/54726
[1] https://doc.rust-lang.org/nightly/nightly-rustc/rustc_driver...
As for the format string at comptime, it's a big improvement over C sure (no missing parameter, yes!) but I still feel that it's still much less readable than Python f-strings.
The main reason not to do it that way is the "you're on your own" nature of dynamism, in and of itself. If you don't come up with a good structure for checking things, the language isn't going to do it for you. And this is fine within the context of the quick hack or the home-grown type system, but less useful for proving broad, general properties about the code. Particularly when this is an industry where the majority of programmers are assumed to be hugely inexperienced, with less than 5 years under their belt, and trusted to build fairly complex systems.
Classical static type systems(your Pascal, Java, etc.) only really know the world in terms of combinations of primitives; they don't prove much beyond simple matching of input to output. But that's yesterday's approach - it's not what industry is looking towards.
What the newer static languages(of the Haskell and Rust ilk) have converged upon is to bolt a powerful constraint solver engine to the type-checking system, which in turn shapes the whole language around pleasing the type-checker. The resulting minimum code quality is often higher, but also semantically torturous.
D has fairly permissive CTFE which uses the language itself; it remains statically typed.
Haskell is statically typed while the type system can be made Turing complete by using certain language extensions.
The Terra language (embedded in Lua) is statically typed and is metaprogrammed using Lua.
Racket is certainly dynamic, but Typed Racket is ... well it's still dynamic, but type annotations are statically checked.
The advantage of not doing it automatically is that the behavior of the code once compiled can be always inferred from looking at the code, no magic and sudden changes in behavior because some threshold has been passed in some optimizer.
Some Rust syntax seems overly confusing. But Nim doesn’t really have strong corporate support backing it.
But, both are still new, and missing a lot of libraries.
Rust is annoying, where there aren’t standardized libraries for common functions. Just some guy’s tweet to use some random cargo crate from someone.
I’m really eager to use more rust, but three optimizations really turn me off. Optimizing the compiler feels like meta programming.