Module wide type-parameters, thoughts

While you voice many valid concerns about things which are easier to do in C++ than in Rust today, I honestly think that you heavily underestimate the maintainability, usability, and robustness benefits of the approaches that Rust has taken by brushing them off as unnecessary procedure and paranoia.

Speaking more specifically about generics and traits, since I think that's where these benefits shine most...

I agree that explicitly writing complex trait bounds is unpleasant. I disagree that it is in any way a comparable trade-off to the user experience of having to cope with template error messages in C++, for the following reasons:

  • Libraries are written once and used many times. In fact, they tend to remain in use long after their development has slowed down or ceased. So if we can (greatly) improve the experience of using a library at the cost of (slightly) degrading that of writing it, it is without any doubt a good usability trade-off.
  • Large libraries tend to be maintained over long periods of time by many different people. On this front, the fact that duck-typed C++ template code is full of unspoken assumptions that are invisible to anyone but its author (and even then...) is a serious issue.
  • Moreover, the roles of library developer and user are not symmetrical. A library has many more users than it has developers, and is in principle built for their benefit, not that of the developer. In this sense, whenever one has to choose between developer and user convenience, users should take priority.
  • The user experience of dealing with template error messages in C++ is purely and simply horrible. Just because you happened to violate an interface contract that the library's developer did not bother to document anywhere, you get flooded by megabytes of incomprehensible error messages originating from deep inside the implementation details of the library you're using, including sometimes from another library that it's using.
  • The user experience of spelling out trait bounds in Rust, in contrast, is not so bad. If you missed a trait bound, rustc will tell you in a concise error message, and usually provide good suggestions of traits that could fill the gap.
  • And if you find yourself needing a certain set of trait bounds many times, you can roll out a "larger" trait which accumulates all these bounds, and reuse that. So trait bounds do not need to be long and repetitive. If you need a lot of them, you have ways of streamlining them.

...and that works until someone tries to instantiate it with a non-suitable type, and gets at best thousands of incomprehensible error message and at worst an implementation which compiles but does not work. Stopping at this point and shipping the library as-is is irresponsible, as it is the generic programming equivalent of the "works for me" customer service antipattern. The fact that too many people do this is the reason why most C++ developers only approach templated code with fear, which I think is not something that the C++ community (which I also belong to) should take pride in.

You need to set a clear interface contract in order to produce robust generic code, and this is what Rust traits (or Ada generics, or the C++ Concepts proposal, for that matter) are about.

I do not think that there is anything which prevents you from following that methodology when writing generics in Rust. Can you clarify?

Unfortunately, duck-typed generic code cannot be very exhaustively tested by its developer. You cannot just test it for every possible input type, as there is an infinite amount of possibilities, and unlike with values there is no notion of "boundary" type that could save you in white-box testing scenarios.

This is where Rust's traits come in: they guarantee you that the compiler will test, at instantiation time, that the input type is right. Since you cannot test it yourself on the type which your user has in mind, let the user's compiler do it.

The thing is, any non-trivial piece of software has bugs. Testing does not reveal all the bugs because it only explores a tiny portion of the parameter space. And static analysis, while powerful, is only able to prove the absence of certain classes of bugs, and highly relies on developer-supplied metadata (like trait bounds in Rust) to do so. This means that some form of run-time analysis is also needed in order to ensure that on the day where the remaining bugs will manifest, the behaviour of the software will remain well-defined.

When a run-time check fails, your software crashes. Bummer. Best avoided, but usually fixable by restarting it. When there is no run-time check, however, you get undefined behaviour: the software can continue to run, but does something totally unexpected by its creators. Like blowing up some hardware, killing someone, leaking cryptographic secrets, or executing arbitrary code chosen by an attacker. This is why unless some absolutely critical performance concern demands the suppression of those run-time checks that guarantee the absence of undefined behaviour, they should remain on even in release builds.

On this matter, I should also point out that the performance impact of removing a run-time check should be measured, as modern compilers are quite talented at optimizing them away in common use cases like array bound checks in a loop. And I should clarify that I am not just talking about run-time checks that are automatically written by the compiler, like array bound checks, but also about those that are added by the developer to check application-specific contracts, such as assertions about the invariants of an object.

Looking at nalgebra, whose interface is conceptually very similar to that of Eigen, I would disagree. Although for this kind of library to feel more "natural", Rust could certainly use some const generics, which are only at the RFC stage at the moment.

Unfortunately, the usefulness of concepts is quite limited if they are an optional feature. We already have something similar to optional concepts today in C++ with things like SFINAE constraints or static_assert, and the end result is predictable: a few people who care about their users use them, most people don't because they can get away without it, and the end result is that the everyday user experience of using C++ template libraries remains terrible.

...and so, you get ill-specified interface contracts, with the same results as with C++ templates: errors get reported deep inside of the implementation, and any sufficiently large piece of code becomes so incomprehensible as to be unusable and unmaintainable. Inferring types in interfaces is okay for small scripts, but it does not scale well to large programs or interfaces which are intended for use by someone else.

1 Like

heh I should stick to the point of the thread but here goes..

Libraries are written once and used many times.
Moreover, the roles of library developer and user are not symmetrical.

I disagree:-

-[1] many competing libraries are written, only a few end up getting widespread use
-[2] any large program is itelf a layered set of 'internal libraries'.
-[2.1] aren't some of the best libraries are extracted from programs? .. e.g. isn't GTK 'gimp tool kit' .. I'm guessing they basically wrote gimp, then decide 'ok, we can generalise our UI utilities for other users. There would have been many attempts at gui toolkits, but one became more popular by virtue of very useful 'demo-program.
as such the roles are symmetrical to me

One of the reasons I'm trying Rust is that I do want to use type-parameters/generics more than I do in C++. (hence the original suggestion, to keep things on topic)

The user experience of dealing with template error messages in C++ is purely and simply horrible

sometimes, but I often used simplified templates that only did what I needed. the error messages are not so horrible, and they're easy to write. (..and we are going to get Concepts eventually). I've got another idea on filtering error messages too for some cases (version-control - find lines changed - filter vs errors )

'std::vector' handles every case. but I can write 2 much simpler 'vectors'.. one aimed at 'tools' (slower and more general) and one aimed at 'runtime' (.. it doesn't even need resizing, because the point of the tools is to preprocess, to vastly eliminate runtime dynamic memory allocation ... but I still have both cases available as a fallback: runtime-libs to optimise slow parts of tools, or tools-libs when prototyping runtime. I've had enormous difficulty explaining this idea in the rust community ("it's a systems language, you need everything to be efficient/safe".. actually ,no,not always; but you do gain by having the efficient and quick-and dirty parts sharing type information , being able to move back and forth, instead of adding the cost of cross-language interfacing. you write lots of exploratory code before you figure out what to keep. this is why we've ended up 'over-using' C++.. for the convenience of having the ability to drop down and interface with the efficient/systems parts.

I have another thread where I suggest a customisation to get a parameterised index in the vector trait. I can do this sort of thing very easily in C++. I can also setup 'dimension checked vector maths' or vector maths that uses custom compressed types easily.

You're suggesting here I have to wait for 'a library writer' (with aligned goals) to figure out the trait bounds .. I've given up trying to replicate some of the things I already had working in C++ .

This also tempts me to abuse rust. What if I made an 'everything' trait with 'unimplemented' just to recover the experience of duck typing.. Or what if I starting writing macros for things that should be functions (again, recovering duck typing) .. but this starts to get messier.

The user experience of spelling out trait bounds in Rust, in contrast, is not so bad.
If you missed a trait bound, rustc will tell you in a concise error message

nope ; just like fixing template error messages, these can stil get increasingly arcane because they still effectively trace through the program (you're baking information from the potential call graph into the traits) . I've had the thing crash spitting out huge errors due to recursion :slight_smile:
You're dealing with the same complexity eventually.

the claim is: 'spelling out a bit more makes it easier'.

  • but we can still do this in C++: we can write a simplified test to figure out what went wrong. . As I explain, my 'path' to end up with working code starts with code that does something , then you adding features and generality. I argue this teaches you more than compiler analysis can - because there's other aspects that still can't be expressed in the type system. ( I like the fact Rust has #[test] out of the box. I can imagine a bit more in that direction, e.g. ways of marking 'this function should be the inverse of that, and here's the empirical test cases' .. 'the output of this function should satisfy these constrains, and here's empirical test cases..' )

I'm not saying compiler analysis is a bad idea at all; I do want as much as possible - but I also want the ability to disable it.

Imagine a rust with an option for whole program inference, and switching off the safety checks. This would be unambiguously superior to C++ in every use case. ; however,

I'm speculating the 'core team'(and their financial backers/mozilla foundation) doesn't want this because they want safe contributions to the ecosystem, as a trade for their work? (I get that people need some reward, even if indirect, hence certain aspects of how open source works).

but I would assert: you'll still 'grow mindshare' faster - you'll get more people using the language, learning the safe libraries. Any 'unsafe' and 'duck typed' crates can be clearly marked and filtered out (by default) for users who don't want it. Or conversely, you might see 'more useful functionality' which gets the ball rolling and hence contributors..'ok lets dive in and clean these up ..'

right at the start of 'jonathan blows language for games', I made the suggestion to him: "just fork rust". He's a very smart chap and has ended up spending x amount of time building a whole new language with different choices, when really what mozilla has done - with a few minor tweaks , should have been perfectly sufficient.

I launched into rust with great enthusiasm, then gave up. I'm back again seeing whats changed (and sure there are some nice improvements). But I could have been a 'ruster' solidly for the past 3 years; and even if only 5% of what I wrote was safe, that would have been more than I've been able to contribute to date. (e.g. I did an HTML source viewer, but let it bit rot in the time I was away.. if you'd kept me using rust for my engine or AI experiments I could have continued with that in the background, and it could have been integrated with 'rust-doc' .. etc ..)

nope ; just like fixing template error messages, these can stil get increasingly arcane because they still effectively trace through the program (you're baking information from the potential call graph into the traits) . I've had the thing crash spitting out huge errors due to recursion

I disagree, being a heavy user of Eigen and have used Boost and other template libraries from very old times when the static_asserts tools were not used as much as nowadays I really would like an example where you can force the Rust compiler to reproduce you the 10MB garbage you get from a single wrong template invocation. The main difference is that with Trait bounds you are never tracing through the program as the Trait contracts apply at each level. If I have a function f<T: foo> and have recursively defined templates using it, each single recursive step would require in the higher generic function to have the bound T:foo and the error will pop out at the top call, it won't dump me the Kraken it does in C++. Again, please do show me an example in Rust where you can do that, I think I can quite easily find a few of my old code where you can get a few thousand lines of error messages.

but we can still do this in C++: we can write a simplified test to figure out what went wrong.

Again, I highly doubt that. You can write a "simplified test" to reproduce to error not figure out what went wrong. This especially evident in very large and long-standing libraries, for instance like the Linux kernel, where errors can often be linked by the superposition of several different changes put together only. And if these changes span on the time axis up to 10 years back good luck reviewing 10 years of kernel code changes.

I'm speculating the 'core team'(and their financial backers/Mozilla foundation) doesn't want this because they want safe contributions to the ecosystem, as a trade for their work?

I would argue here, and this is expressing my opinion and impression, that it is not only the core team but a big part of the Rust community that wants safe contributions to the ecosystem - it's pretty much the main appeal of using Rust in the first place. If there are going to be a bunch of very standardly/commonly used libraries in this language which is unsafe and 80% of all Rust code will rely on those, then why even bother using Rust? Yes traits and a few things, but at least for me having the significantly stronger guarantees that things are safe for using in production is the biggest selling point of why I would waste my time learning this significantly more complicated language (in terms of at the begging the effort is quite steep while you wrestle with the borrow checker and get the hang of it).

but I would assert: you'll still 'grow mindshare' faster - you'll get more people using the language, learning the safe libraries. Any 'unsafe' and 'duck typed' crates can be clearly marked and filtered out (by default) for users who don't want it. Or conversely, you might see 'more useful functionality' which gets the ball rolling and hence contributors..'ok let's dive in and clean these up ..'

And it would make such a huge mess and pandemonium for maintainability. Assume you use the "safe" library. Then the library adds 2 features, one of which you want to use and need and one which now uses some other "unsafe" library. Suddenly, in your use case of the library, you get a security vulnerability cause something can segfault, but the original "unsafe" library has not been intended for this so they are obviously not responsible. Also on how much fine-grained control, you will have over the "safe" library thick? Are you going to have to decide for every single of the whole 1000 crates in the downstream dependency tree if you are going to use its "safe" or "unsafe" mode?

At the moment, nothing stops you from creating an unsafe library just to showcase an idea and then if you think its cool you can convert it to safe, but only then publish it. I don't think this harms the process of developing new things.

Finally, I want to discuss all this things about template vectors. I personally am really strongly against having anything like what Eigen is in general. Why? Because they are essentially writing their own compiler in the Templates of the source language. The result - you get this horondous messages. What one should be doing, for instance in Rust, is basically write the compiler properly, without the templates, and than make a compiler plugin, which to essentially optimize the intristics of your structs. Than you can get very nice messages and get the exact same performance with compilation. The tempalte solution in my opinion is just a bad work around what you wanted and should be doing, just so that it can blend in the language. With compiler plugins in Rust that is pretty easy to do without even needing templates what so ever. So to the point that you can't write Eigen in Rust - probably not, but thank god for that. On the other hand you can write a compiler plugin which to do the same as Eigen, but be a lot more maintainable and user friendly.

Part of the 10MB is your own source code. the process of doing the same thing, you have to express the same information.

I'm finding it harder to reach an end result, as such have given up trying to do some things that I find easy in C++.

some of the libraries are to big and bloated, it is the fact they try to be all things to all people that makes them hard to use.

I can write templates that handle the cases I need.

**My motivation to use Rust, sorted by priority:- **

  • [1] no header files
  • [2] better lambdas (via 2-way inference, and just plain better syntax |x|..)
  • [3] 'immutable by default' , unsafe globals (thats the one restriction I DO agree with)
  • [4] enum/match (this feature is awesome)
  • [4.1] 'everything is an expression', i'll list it here where it shines.
  • [5] better macro system/ 'reflection' type uses cases, e.g. easy serialisation.
  • [6] tuples (I really enjoy the ability to anonymously group values, in a syntactically lighter way than 'std::pair()')

But to get these - I'm finding the traits (loss over general overloading) and full safety a hinderance; and I can't just wrap unsafe{} and use raw pointers because the remainder of the raw pointer use has been (seemingly) . Either one of those on their own might be ok; it's the combination of both that begins to seem oppressive.

C++ is used for control, but damaged for productivity in awkward ways by mere syntax (headers, misuse of premium , chars). I really do just want C++ cleaned up. rust came with a load of 'bonus features' (like match) which are very interesting.

It should be possible to pick and choose exact blend of features I want. Software is the most malleable medium on this planet.

What happened along the way is some features I liked got removed (~T ~[T] .. syntactically light smart pointers and vectors counted for a lot, playing well with () to make light function signatures ... I would have gone further and added [K=>V] for maps giving very light signatures for a lot of common code, note Swift does also have [K:V] so my notion isn't entirely spurious .. a tech giant agrees; 'do notation' for internal iterators.. dropping the nesting level for lambda-based code; Whilst some features landed in C++ .. which is why I didn't stick with it originally)

At the moment, nothing stops you from creating an unsafe library

(addressed above: beyond merely needing 'unsafe', the language syntactically discourages raw pointers in other ways)

for a better insight into my POV, I agree with about 90% of what he says https://www.youtube.com/watch?v=TH9VCN6UkyQ&list=PLmV5I2fxaiCKfxMBrNsU1kgKJXD3PkyxO .. he discusses how he considered Rust,D,Go as alternatives and why he rejected all 3 to continue.

All it would take is a few options to relax things and it would open this language up as a definitive choice. Just unleashing the full inference alone would make things more pleasant ( i note in haskell, you don't actually have to implement all the functions in a typeclass; we can do 'unimplemented!()' but we could recover the C++ use case as an 'opt-in' if there was a 'compile time failure' - a stronger 'unimplemented' ).

Sure, having the types in functions is a reasonable compromise but sometimes you have working code, but then you want to extract a function... but that's hard to do because you now have to figure out the signature (which might have complex intermediate types)..

As it stands.. I have to keep going with C++ (modules will fix [1], I can resort to a #define for compact single-expression lambda), and possibly wait for his language (his goals align more closely) ... or even continue with my own (which would get no other users, but I could inter-op with C++ better by supporting more of the feature set GitHub - dobkeratops/compiler: C/C++ subset resyntaxed like Rust,+ tagged-union/Pattern-Matching, UFCS,inference; LLVM . example:).

I hear that Swift is actually going to get move/move-semantics, that might be another option

Then the library adds 2 features, one of which you want to use and need and one which now uses some other "unsafe" library. Suddenly, in your use case of the library, you get a security vulnerability

I don't see that happening, surely the 'unsafe' can be correctly flagged through the calligraph and or module dependancy graph if it really is an unsafe module. (linking to unsafe crates would be blocked unless you deliberately asked for an unsafe build).

But 'unsafe crates' continue to exist of course, i.e. all the bindings to existing C/ C++ projects.. they're not going to re-write OpenGL, Vulkan, all the video codecs etc etc etc in Rust.

bear in mind this conversation is conflating 2 things, unsafety, and Rust other 'oppressive choices' (compulsory traits and no overloading). just loosening one of those would ease things a lot. the 'unsafe' case will remain; C++ wont go away, there's massive sourcebases in regular use, and for most use-cases the solution is to layer an 'application language' ontop (most of my friends are busy using C# now and loving it)

I'll check how the 'intrinsics story' is going in Rust BUT
the fact is machines change, ISA's change.. in the past it was console gamedev; in future it might be new chips for AI (what if they start making RISC-V with custom accelerator stuff, what about the movidius chip..) C++ with intrinsics gives you a blend of customisability and optimizability out of the box that is hard to match; not everyone has time to customise their compiler or study it's internal API

I can tell you that the work i've done 'in anger' was all about 'getting shit done' on new platforms before the tools were ready, hence having a competitive advantage (being on a platform before rivals) That meant being able to drop down to ASM for custom instruction use, Microsoft took a step forward by actually enabling C-like intrinsics but originally those weren't accessible if you wrapped types in 'classes', only a pure intrinsic typedef. They fixed it eventually but if you architected your code reliant on that.. you missed the critical first/second wave window .. your product releases along with a flood of competitors.

I'm not doing this right now, I am looking into 'other languages' for interest.. but the point is if we are talking about the ambition of a C/C++ replacement - it must be able to handle the same use-cases.

If i just wanted to 'ship apps' like anyone else using existing libraries there's swift/C# etc .. and you get the use of optimised OS services (if you really want to defer all that to some one else).

I think it's possible to improve on 2 of 3 axes - performance,productivity,safety, but maybe not all 3 simultaneously.

On that last point about new chips its pretty much why the LLVM is split to front-end - middleware - backend so I don't see that ever being an issue. You can do the same thing on various levels to squeeze that performance - specifically, since you are talking about AI look at Theano and Tensorflow - they are compilers embedded in python.

It is still heavier work to customise a compiler. The scenario I describe was before LLVM but microsoft always had their own compiler team .. the point is it's another thing you have to wait for, it takes a finite amount of time to fix. Intrinsics are actually a nice level to work at IMO because they expose a closer mental model to what you're trying to optimise for.. without having to go all the way dropping to ASM (I actually didn't like wrapping this stuff in C++ classes so much, but eventually it was possible and we did it, and many people did like it. Conversely ASM still got used for squeezing every last drop out..). Anyway thats just one of several cases. The other is dealing with a multitude of compressed data formats . expressing the types and their transitions can be hard but the consistent rules of conversion operators can handle things for you , and you can insert checks in the debug build. this is a philosophical point but as my "airplane control software example" shows, Rusts talk of safety is still one point on a sliding scale with omissions.. it doesn't eliminate the need for empirical tests.

what we also had was a situation with branches being performance killers.. runtime tests had to be minimised.

This sort of thing still happens, e.g. the move from GL to Vulkan is partly about reducing the amount of runtime validation being done.. a lighter driver that can assume correct state, putting the responsibility for buffer management in the hands of the application, because the 'nice safe' interface can't exploit whole-program assumptions

if you have a potential hazard like 'divide by zero', the program has to avoid it through high level logic (e.g. filtering out degerate triangles before you compute normals, then you know 'compute normal' doesn't need a divide by zero check. etc etc)

[quote="dobkeratops, post:17, topic:11792"] since you are talking about AI look at Theano and Tensorflow - they are compilers embedded in python.
[/quote]

these are pretty cool,
but what my thinking relates to is at the level of the work google must have to do to get this all to work in the first place.

there's their 'TPU' which is a low-precision matrix multiply engine; I anticipate that we will see more units (more like the Movidius vision chip) .. at the minute the world is using GPUs but they come with a load of extra complexity geared for rendering; an AI chip would still want more versatility than the plain TPU (think about the potential ways in which weights could be compressed) . they'll go through their own path of evolution. personally I think the ideal device will be like the 'adapteva epiphany 5' wth more control over local memories , but i've no idea what the state of the art is inside MS/google/apple at the minute.

some people think plain CPUs and GPUs will continue to rein supreme, but I'd argue AI/vision is going to be a big enough area to get it's own fully dedicated units.

I know Rust is of interest for IoT (intersection of online and embedded).

the gamedev case is interesting: jonathan blow explains very well the ways in which rust still hampers the 'exploratory/arty' coding. It doesn't need to be 100% safe or performant at every step, but we do need to be able to handle both extremes (performant, and productive) and alternate between them; we do make big programs (100klocs), but not huge (mlocs ). Some of rusts decisions are all about things that matter for mlocs but not klocs.
The "exploratory" side was important enough to integrate scripting languages (Lua etc) for , but to have that 'inline' , sharing type information , with no 'interfacing' overhead (either performance or tooling/boilerpllate) would be awesome.

Mutliple types of code in gamedev..

  • Tools both UI, and asset conditioning pipelines
  • gameplay (scripting , 'high level' c++)
  • engine (low level C / C++ )

... but you might need to migrate between use cases, which is why we over-use C++ (as needed for the Engine).
from what I've seen swift would be superior already for the first 2 cases... but Rust could be too with some changes.

this is why I miss the Sigils so much. they made the common smart-pointer types 'melt-away' enough for old-rust to 'feel like' a productive language.

r.e. the 3rd case, one interesting thing Jonathan Blow says is he doesn't even consider std::vector / Vec performant enough; he explains the need for raw pointers for 'joint allocations'. the 'blob' approach of loading precompiled levels. you could of course do a validation pass to verify things are ok, but you're into a realm where 'runtime safety' has become less clear cut

I think a lot of the friction you are experiencing is because when people say "Rust is a replacement for C++", you feel like it should be a drop-in replacement and should be able to use exactly the same patterns (e.g. advanced templates, as has already been discussed). Instead, Rust is its own language with its own way of doing things and has its own opinions.

Regarding the full safety "hinderance", unless you have done extensive profiling and can definitively point to places where things like bounds checks are having a negative effect on your program, I don't think your argument holds much water. Improper indexing is a programming error, and as such I would much prefer my program to blow up at run time (yes, even in production) instead of silently accessing the next bit of memory, opening the door to things like buffer overflows. These kinds of safety guarantees are the reason people will switch to Rust from C/C++ in the first place and are integral to the language.

If you want syntactic sugar for defining maps it's not overly difficult to implement yourself with a macro.

macro_rules! hashmap {
    ($( $key:expr => $value:expr ),*) => {{
      use std::collections::HashMap;
      let mut map = HashMap::new();
      $( map.insert($key, $value); )*
      map
    }};
}


fn main() {
    let some_map = hashmap!("foo" => 1,
                            "bar" => 2,
                            "baz" => 3);

    println!("{:#?}", some_map);
}

If you are talking about stuff like using auto as the return type from a function, I believe the language team (and I agree with them) said that Rust would never support this because you should only ever need to look at the signature for a function to determine what it does.

Trait bounds also help ensure this, you can tell at a glance that fn for_each<I, T, F>(iter: I, predicate: F) where I: Iterator<Item=T>, F: FnMut(T) takes an iterator and a function which can be run on the items. I'd argue that this is a lot more explicit than C++ templates and prevents the massive error messages because it's easy for the compiler to say "you gave me an u32, but I expected an iterator".

The existing inference system is more than capable of inferring function signatures for us, but then if it does that it means you'll need to sift through an entire function's source code to see what types it uses. Code tends to get read a lot more than it gets written, so Rust decides to trade short term developer convenience for readability, long term maintainability, and usability.

please please please stop right there.

In my historical use case, a comparison and branch instruction out of place was enough to cripple performance (because it prevented other optimisations) - routinely 10x, and as much as 50x in the most extreme cases.

Now, this might not be the case today when you run your program on a typical big CPU, but the point of something as universal as C andC++ is their ability to be used in every conceivable niche; and machines continue to change, not just get 'bigger and faster', but more parallel , or smaller .

Note now that we do have intel trying to generalise their instruction set for general purpose vectorisation (e.g. vgather) .

I don't know what situations the future will bring, but if i'm going to replace C or C++, I must know that it can handle the situations I've dealt with in the past.

there are situations where an 'error message' is unacceptable behaviour .. and there are situations where conditional operations cost way more than you might think (because of branching / vectorization / deep pipelines .. ).

if you use this philosophy "don't fix it unless it shows up in the profiler" .. you can end up with a program thats slow because everything is done like that , lol, .. no single part shows up..

you only ask for the operations you need. and in a complete/'finished' program, you don't need error checks

(edited for less shouting lol)

". Improper indexing is a programming error, and as such I would much prefer my program to blow up at run time (yes, even in production) "

.. then thats a debug build, not a release build
you can certainly switch the naming
'C++ extreme debug' = 'rust debug'
'C++ light debug' = 'rust release'
'C++ release' = 'An option yet to be added to Rust'

when we did games on these consoles, we had soak tests and debugging and we simply weren't allowed to release the product until there were no such failures. (People are looser today with downloads and PC platforms). safety was due to the platform being a walled garden; you wouldn't get content onto the machine without going through their channel.

"These kinds of safety guarantees are the reason people will switch to Rust from C/C++"

nonsense.

if you really want bounds checked arrays, they're trivial to implement in C++. (and sure, we got things working by implementing that and way more besides.. floats that would check themselves for being non-Nan, to track down the problems).

People aren't seriously considering throwing away their sourcebases and experience over something that simple.

we can also retrofit static analysers, we could even label the references if we really needed to.ref<T> alongside a the successful default assumptions rust has discovered ('most are accessors where you assume the lifetime of the first parameter..')

Conversely , if you want to get rid of header files , or get a match template to infer the return result .. thats really hard to get in C++.

"Regarding the full safety "hinderance",

part 2- one thing i'm repeatedly trying to explain: there are conversely times (and areas of a program) where flawed behaviour is better than no behaviour (because you're trying to learn something else).

the most direct example is debug code; I don't get why this isn't clearer. Unless your brain has an embedded digital computer , you're eventually going to write some sort of tests alongside your regular program - as such having a "productive" language embedded right in the same source files as your 'performant/'safe' language is useful

the missed opportunity here is the whole program inference - if you're writing a test for a function next to it, one or the other is going to give the information needed

If you want syntactic sugar for defining maps it's not overly difficult to implement yourself with a macro.

I know you can do that for declaring maps
I mean for maps in the function signatures; apple do this [K:V] .
function signatures are important because thats what you search

(repeating myself) the original rust with the ~T ~[T] etc showed unique blend between readability and performance.. it was just right. You still have a concrete symbol telling you the important information ('this is a pointer to an allocation..') but it's light enough to melt away and let your mind focus on what you're actually doing. I'm amazed that people complained about it ('too many pointer types'..) .. they were very logical ('owned allocated version of..') Box<T> is more like 2 extra mental steps because it's a word and a nesting level (and yes I hate writing unique_ptr<T> in C++.. there's always a serious temptation to revert to raw pointers. if the thing you're supposed to use is syntactically light.. thats very helpful)

there are little tweaks that could recover this. if we had the option for more inference, the signatures that you do write wouldn't matter so much;

one idea that would turn the traits from a hinderance to a virtue (in my mind) is: if when impl-ing, you could elide the types .. just copy them from the trait. Then I wouldn't mind them. (I might write an RFC for this). Haskell of course lets you work like this - thats why I didn't find the 'typeclasses' as oppressive over there

The frustrating thing is seeing this amazing underlying language engine (coming from C++ I do find the level of inference very impressive .. and I do start to miss it back there ) .. but then some of these decisions taken toward verbosity

Well if you've got numbers like that then you've already profiled and know that bounds checks are hurting your performance. I was saying to profile first because so many people jump to conclusions on why they think something could be slow, without any real scientific evidence.

That said, if bounds checks are hurting you what's wrong with the unsafe get_unchecked() method? It's not the default because it voids memory safety guarantees, but doesn't it do exactly what you are looking for?

I can understand why you feel it's deciding towards verbosity over convenience. Forcing you to write out and understand the full type signature is verbose and requires more characters, even if it does make things more readable for others.

You might want to post a thread on the internal forum to see what people think about expanding type inference to inside a module. Maybe limiting it so that things being exported with pub are required to have a full signature, but internal stuff can be inferred. I'm not too sure how successful this would be though because it's a slippery slope to having APIs with auto in them, leaving users of libraries guessing what type of thing they're getting back. It's also going to be really hard to define an exact boundary because there will be edge cases which blur the line.

yes, you're right that profiling is useful, because in many cases the real issues are counter-intuitive or subtle.

But what I've also seen is this philosophy results in bloat : "According to the profiler.. bounds checks don't slow it.. virtuals don't slow it.. the dynamic allocations don't slow it.. the lack of LOD doesn't slow it"

yet it's slow.

no one thing shows up.. because you've taken those slow architectural decisions across the board, and your code is drowning in them.

I know most cases are not so specialised.
I don't doubt there's times when bounds checks are the right choice, and I can certainly accept them being default. but when push comes to shove, a systems language suitable to replace C or C++ in every possible niche needs to let the user reason explicitely about every possible operation.. or lack of.

Forcing you to write out and understand the full type signature is verbose and requires more characters,

One suggestion I have - I've made an RFC - is to allow ommitting the types when you 'impl' a trait; there the types have already been defined by the trait (haskell allows it).

This would make the trait more obviously useful ("it defined the pattern -> no need to write the pattern out again").
That would be an example of synergy. You need the original trait for reference; rust's nice syntax does make searching for traits easy aswell.

slippery slope to having APIs with auto in them,

Tair enough, thats a problem. Perhaps full inference for private is a reasonable compromise. What I would want is types for crate level exports, and the option of full inference everywhere else

With more options, you can pick the correct tradeoff per situation.

1 Like

one thing about verbosity - back in C++ there is still the option of falling back to raw pointers which combine 'Box', 'Option<Box>' , 'Option<&T>', maybe even a List / Option .. with an intuitive guess as to which being possible (get_ vs find_ vs create_.. vs take_..). (You might even argue that with 'unique_ptr and &T existing, the *T in C++ has a viable use as Option<&T>)

What I liked about old rust so much was that the 'unique_ptr' replacement '~T' was as easy to read and write as *T . when it's a single character it melts away. conversely the when it's more verbose, it's more irritating when it's compulsory.

Now if you had whole-program inference, it wouldn't matter so much if the types are more verbose; and of course encoding more machine-checkable information in the type system is definitely a good thing.

Is discussion about the mod qqq<T> { } going to continue or the thread is derailed irrecoverably?

1 Like

Is the discussion about the mod qqq { } going to continue

Well I'm still very interested in this, so if anyone else has any input on that it's very welcome.

Regarding the original proposal, my main issue is that there's been no suggestion or discussion on how this changes client code, so it's really only half of a proposal which makes it hard to comment on meaningfully.

So assuming the use of this feature in your library looks like this:

mod<T> {
    struct S { t: T }
    fn foo(s: S) -> T { ... }
}

Would client code just be not affected at all?

use dobkeratops::mod;
fn main() {
    mod::foo();
    mod::foo<i32>(); // still valid, right?
}

This would mean some foo() call sites no longer match foo()'s signature in the library.

Or would the type parameter be applied to the module import instead?

use dobkeratops::mod<i32>;
fn main() {
    mod::foo(); // no type parameter allowed here
}
use dobkeratops::mod;
fn main() {
    mod<i32>::foo(); // <i32> must be on the mod, not on the foo
}

This seems to rule out ever importing a single item and passing different type parameters to it. You'd have to either import dobkeratops::mod<i32>::S and dobkeratops::mod<f32>::S separately or only import the mod and always say mod<i32>::S or mod<f32>::S instead of just "S".

I'm certainly on board with the goal of reducing verbosity in Rust generics, but right now this idea is incomplete and I don't see an obvious "completion" of it that does reduce verbosity without introducing other problems.

For comparison, implied bounds is an orthogonal but probably overlapping idea for verbosity reduction that seems to me like a huge improvement if we can just get the details right. I think a lot of the discussion on that thread is relevant to getting any similar proposal like this one into a state where it could be seriously considered.


P.S. Okay, I can't completely resist the ideological arguments, so just one point there: In my experience I am far more productive with "strict" Rust generics than I ever was with C++ "duck typing", because in Rust I rarely have to think about both the details of my generic function's implementation and the details of its call sites at the same time (much less the implementations of several other generic functions that mine was calling). That increased modularity and encapsulation is the real benefit of enforcing proper bounds on generics. Rust having better error messages is merely a (very nice) side effect of that.

2 Likes

I freely admit I have not thought out all the details here; however the existing ability to nest parameters suggests there should be a valid path to discover here.

Or would the type parameter be applied to the module import instead?
I think it would be more like the former example, where the module typeparams mostly get inferred by context just as normal type-params do. However when you put it like that ('importing a module with dedicated specification'.. I think you'd expect to be able to do that aswell, just as you can manually assign other type-params when you want to. 'use vecmath::<f32>;' // default precision

I have another more unusual/speculative idea to discuss in another thread, i'll link back to here ,it's closely related. 'module wide' shared function parameters, TLS, dynamic scope emulation)

I support this.

I'm working on an interpreter. Programs read streams of bytes and write streams of bytes, but rather than forcing STDIN and STDOUT for IO, I'd like it to be parameterized over two types R: std::io::Read and W: std::io::Write, as this makes things such as testing easier. This means that almost every struct I'm defining in the program includes an <R: Read, W: Write>, which is quite noisy. I'd prefer if I could simply make the module parameterized over R and W.

I'd have two syntaxes for declaring parameters:

  • You can declare parameters from anywhere inside a module by writing

    mod<R: Read, W: Write>;
    
  • A module defined with the mod foo { } syntax can be given parameters as

    mod foo<R, W> { }
    

Say we have a parameterized module foo<R: Read, W: Write> defined in some other file. It would be imported just like any other module:

mod foo;

Any attempt to actually access items in this module, however, would require that it is somehow disambiguated:

    foo::<i32>::bar();
    use foo::<Option<Vec<usize>>>::Baz;

In the example for the interpreter, main.rs would look like:

use std::io::*;

mod interpreter;

fn main() {
    let src: String = /* read a file */;
    interpreter::<Stdin, Stdout>::interpret(&src, stdin(), stdout());
}

#[cfg(test)]
mod tests {
    fn run_and_collect_output(src: &str,
                              input: &str) -> Option<String> {
        let output: Vec<u8> = Vec::new();
        interpreter::<&[u8], Vec<u8>>
                   ::interpret(src, input.as_bytes(), output);
        output
    }
    #[test]
    fn test() { ... }
}

Perhaps there could be some inference for this, the way there is for type parameters in functions. That way perhaps ::<Stdin, Stdout> and ::<&[u8], Vec<u8>> could be omitted.

In addition (or alternatively), perhaps there should be some way to "distribute" the type parameters to a module's constituent items. For example, say a math3d module was parameterized over F: Float and exposed

pub struct Point(F, F, F);
pub fn distance(p1: Point, p2: Point) { ... }

It should be possible to turn math3d's type parameter into a type parameter of distance so that you could import it generically.

use<F> math3d::<F>::distance;
/* `distance` will now behave as if it had a type parameter F */

To import math3d in such a way that all of its items had its type parameters distributed to them, you could write:

use<F> math3d::<F>;

This way, math3d would become unparameterized and all of its items would take the parameter instead (the way the module would be written today).

Syntax is up for bikeshedding, but that's the general idea.

1 Like