Greetings, Over the thanksgiving break I took some time to write an application of medium complexity in Rust in order to get a better understanding of how the language works in practice (up to this point I had never even installed the compiler, and was merely an interested spectator of the language's progress).
So I wrote a ray tracer, capable of loading up a restricted set of .obj model (only triangles, no normals, colours or textures), and then ray tracing it using a kd-tree for performance. Here's a picture of a cow, as an example of the output http://i.imgur.com/80E2F.png (this image took about 6 seconds to render, on a single thread on my core i7 2600.. although I wouldn't read too much into perf. numbers for now) I've attached the code. It's a mixture of quick-n-dirty hacks, and some attempts at trying out various kinds of modularity. Hardly a paragon of code quality, but I really don't have time to clean it up. (the cow model referred to in the source code was downloaded from here http://groups.csail.mit.edu/graphics/classes/6.837/F03/models/). I figure I'd write up a quick experience report. The point of this is for me to point out the rough edges I experienced in the hopes that they would be considered for future revisions, so it will necessarily be somewhat negative in tone. This is simply because positive feedback is less actionable, and not because my overall impression is negative. And I'm sure you're well aware of positive aspects already (it would've been part of your design rationale, in most cases). I mention this in advance in hopes that you won't take the following as any kind of sweeping criticism, but merely the few things I felt were getting in my way. Also, I'm not 100% sure of all of these points. Quite a few of them might just simply be me not being able to figure out how to do what I wanted. I was hoping to go back and look into each of them in some detail to be more certain that there's actually an issue with all of these things, but it's already been a couple of weeks and I simply haven't found the time, so I figured I'd write up my concerns such as they are, and if some of them aren't valid then you can just ignore them. Without further ado, here's the summary of things I ran into: 1. Purity, or lack thereof. Firstly, I found the "pure" keyword went mostly unused. I tried using it initially, but very quickly ran into places where I couldn't (e.g. the sqrt function isn't pure, so now my vector length function can't be either). So after an initial attempt at always defaulting to "pure" I basically gave up and used it nowhere. That's a shame. Also, I can't use the pure keyword for functions with local mutable state. For a language with such imperative feel as Rust, it seems that disallowing even local mutation would make most functions impure, even though they're not actually impure in any real sense. Something like the ST monad in Haskell (which allows you local mutable state, so long as it doesn't leak), but less in-your-face would be nice. In other words, I'd like not only local mutable stack variables, but also local heap allocations/modifications, so long as none of that memory "leaks out" outside the function. Second, I'd caution about making purity the exception, rather than the default. Having the ability to specify that you expect a function to be pure is great (e.g. for parallelism, or laziness), and it's likely that this will enforce quite alot of cascading purity throughout your program. However, I worry that this will happen "after the fact", in other words that you will ocassionally have critical moments in your code when some library call requires purity and you have to go back and modify hundreds of functions because of the cascading ramifications of making a single function pure. It would be better if everything was pure by default (assuming that local modifications are allowed, as mentioned above), with the very few truly impure functions around being explicitly tagged as such. This is somewhat fuzzy, but I got the same kind of uneasiness I get in C where I'm not quite sure that something is pure or not because the compiler won't enforce it (in Rust's case because I didn't add the pure keyword, for reasons stated above). It doesn't feel quite as safe, because calling a function may or may not have subtle hidden effects, and you can't really rely on the "pure" keyword to make this distinction, because even things that actually are pure are likely to have missed adding the flag (see "const" in C++), so you'll still find yourself wondering "is this actually pure where the author forgot to mark it as such, or is it impure?". In general, when asking about what the default behaviour should be, I feel like there are two criteria to consider: a) Which is the most common case b) Which case will cause mistakes to show up faster If purity worked with local mutable state, I feel that pure-by-default would win both of these (most of my functions were certainly pure, and using impurities in a pure function would cause an immediate compiler error, whereas you could go months forgetting to tag functions with "pure" without the compiler giving any indication that you're at risk of a major "purity-cascade" if a future revision needs one of the toplevel functions to be pure). 2. Mutable locals I found it annoying that local slots were mutable by default, for a few reasons. Firstly, it's inconsistent. Rust documentation states that things are immutable by default, but that doesn't actually apply to local variables. So it's inconsistent w.r.t. parameters, as well as w.r.t. record/obj fields. Second, because mutable locals incur a fairly high tax on readability, IME. This is again due to the fact that you can't really rely on immutable locals to be marked up as such (side note: I don't even know how to do this, is it possible?). Again, witness const in C++; nobody I know bothers tagging local variables as const. The result is that when reading a new function, you have to spend considerable effort scanning through the code to find all the places each variable is modified (even though most of them never are), in order to understand the data flow. By the criteria above for choosing a default, immutable-by-default wins both criteria for my code. Over 90% of my local variables were actually immutable (though none were tagged as such). And mistakenly forgetting to tag something as mutable would give an immediate compiler error, whereas forgetting to tag something as immutable would go unnoticed until someone in the far future gets annoyed by unclear data flow and adds the right annotations in bulk (which I do on occasion in C++, but is a rare occurence!). 3. Ownership of unique-ptr I find that it's quite common to want to say "this is a pointer to a heap box that is both uniquely owned, and will never change owner". E.g. where you really want something conceptually close to just storing the value "in-line", but need to put it on the heap because you're referring to yourself recursively (e.g. a tree). Unique pointers give you a way to say that there's only one owner at a given time, but I'm not sure if there's a way to make sure ownership is non-transferable. In fact, this should probably be the default (i.e. only mutable unique pointers can lose ownership of their memory). Also, I had a few instance where I wanted to work with unique pointers in a bit more flexible way than just passing them to a function, but I didn't want to copy or take ownership of them. E.g. in kd-tree tracing you want to traverse the two sub-spaces in different orders depending on which is closest to the ray origin, the rest of the code is the same so it's nice to say something like: let (near,far) = origin < splitter ? (node.left_tree, node.right_tree) : (node.right_tree, node.left_tree); And have near and far simply be immutable references to the two sub-trees. Unfortunately this causes a copy of the unique pointers (resulting in an order-of-magnitude perf. hit for this application). There may be some way using explicit types to do what I want here, but really it seems like copying shouldn't be the default. Perhaps if locals were immutable (see point 2), they could also be immutable references by default if the value you assign to it is immutable. I ended up switching to shared pointers, even though I really would've preferred to specify unique ownership of the sub-trees in the kd-tree data structure. 4. I got tripped up by the mandatory literal postfixes about a million times. This may be just a habit thing. 5. Linear algebra looks really clunky without operator overloading. I mean, look at this: add(scale(n, dot(view_vec, n)*2f), view_vec) 6. I seriously couldn't figure out how to use the pow function. No amount of importing/using seemed to bring it into scope. :-/ Overall, I found Rust to be quite enjoyable to work with, even though I'm used to a higher level of tooling support (oh how I miss intellisense). Look forward to seeing where it goes in the future! Regards, -- Sebastian Sylvan
main.rs
Description: Binary data
math3d.rs
Description: Binary data
model.rs
Description: Binary data
raytracer.rc
Description: Binary data
raytracer.rs
Description: Binary data
_______________________________________________ Rust-dev mailing list Rust-dev@mozilla.org https://mail.mozilla.org/listinfo/rust-dev