Rust's language ergonomics initiative
blog.rust-lang.org
blog.rust-lang.org
I feel like there's a real lack of user testing in software development tools land. If you're developing software for unsophisticated users it's obvious that you should be doing user testing, but it's an often ignored fact that developers are users too! APIs, compilers, build tools, they could all be vastly improved with some user testing.
There's always room for improvement, but iterating in this way is defintely a core value.
You are right however that you need to carefully pick the tasks to fit with the original aims of the language.
Perhaps a good way of phrasing this is "why aren't X people using rust for Y?".
https://thefeedbackloop.xyz/stroustrups-rule-and-layering-ov...
The HTML5 spec was developed in this way, and some of the code-health folks at Google were just starting to operate this way when I left, but I haven't heard of many other language-design efforts working like this. The technology & data exists now, with large public corpora of open-source code, parsing libraries that can identify patterns in it, and big-data tools to run these compiler tools over a large corpus. It's a bit of a paradigm shift from how languages are typically developed, though.
https://github.com/golang/proposal/blob/master/design/12914-...
Compilers actually have a unique opportunity to introduce some form of opt-out tracking of all of the compilation errors with sources, etc. This could really help to understand the users.
I am not sure if it's something that ever landed or not...
Thankfully, Mozilla is one of the few entities left in the technology industry that seems to care about ethics. I don't believe the Rust team would ever do something like this.
And Mozilla is the one with opt-out of all of the unethical features in Firefox. They have no ethics, looking like they care about those things is nothing more, but PR. After all, corporations are not people.
This means the user might have struggled a long time with the code before even hitting the compile button. If you only collect statistics from compilation you will miss all this pre-struggle.
That being said, this is really hard. Observing a (necessarily small) sample of heterogeneous programmers dealing with (necessarily small) tasks isn't easy, either.
I wonder whether users would consent to having their text editors and compilers instrumented to produce a user study of every single character typed and every single build (failed or successful, especially failed). I would imagine that a language designer could learn a huge amount from seeing all the things their compiler is rejecting from real users...
I feel like a lot of what I see is people want to establish that language X is much better than language Y, and I think that kind of study almost doesn't make sense with all of the relevant but poorly defined and difficult to control variables. A study around ergonomics of a few options in otherwise the same language is comparatively trivial.
It emphasizes ergonomics, actually, as well as making functional best-practices (typed immutability, etc) more accessible.
I'm in San Francisco but happy to conduct remotely. I'm targeting people familiar with ES6 and functional styles, but interested in folks of all experience levels.
If anyone would be interested in trying it out, shoot me an email at rattray.alex – gmail.
I'd add: Do you want to focus on new or experienced users? For example, when implementing systems that will be used 8 hrs/day by its users, we look only at efficiency for experienced users (unless the political situation requires placating the noobs). They will be noobs for a day or maybe a couple weeks; they'll be experienced all day, every day for years. We explain it that way at the beginning and they thank us later.
An example many are familiar with is Vim: Steep learning curve, but I'm so happy that the focus is on efficiency for veteran users.
If you're implementing a system that users have to use for their job, they'll put up with whatever they have to while they're learning it. If you're implementing a programming language and you want to increase adoption and general user friendliness, I think this is a terrible choice.
I love Clojure now and I work with it every day, but it's still frustratingly hard for avoidable reasons even though I'm a pretty advanced user. I'm jealous of the attention that Rust, Elm and Elixir pay to this aspect of language design, and I'm very pleased to see languages taking it more seriously.
If something is a pain point for the noobs it's usually a badly designed part of the language that the "veterans" have just learned by rote to step around.
Even if no actual language changes come out of the new users group, just documentation updates, it would be a win for the entire ecosystem.
I am not sure whether 10 minutes of Rust would give you actionable info, I mean it certainly takes longer to get familiar with the basics in almost any language, even if the 10 minutes just stands for a short amount of time.
It's not appropriate to limit user studies only to people who are already experts.
I don't think there's any empirical support for the idea that a shallow learning curve translates into a powerful/expressive language. EDIT: I'd love to be proved wrong, so if there's anything in the way of evidence, please link me!
Undoubtedly, it's a language that is a little tricky to get fully fluent, but I'm curious what things pop out to you as bad ergonomics? I think it actually doesn't suffer in many of the ways identified for Rust in this article.
EDIT: Well, that rant turned out longer than I imagined... and it's even later now, so I'll probably go to bed momentarily.
EDIT#n: PS: Scala's still better than Java, but please don't tell me Scala's in any way "coherent" or "well-designed" or "orthogonal". Haven't really tested Kotlin or Ceylon, but I get the feeling that those are really much more c-or-w/d languages for the JVM. Plus, "orthogonal" doesn't mean shit if your ergonomics suffer like they do in Scala.
First: See Scala Puzzlers + PaulP's rants... and that's not even half of it. (Yes, I know collections are going to be "fixed", but... not really. I don't think the fact that Odersky still seems to be calling all the shots on that front is in any way encouraging.)
Second: An especially annoying thing is the way "_" desugaring works which sometimes leads to really surprising results when it turns out that "no, you cannot use _ to stand for 'identity' in a function parameter context". This was a conflation of 'features' namely that scalac doesn't warn when a unit-returning method is actually with an expression that doesn't return unit. If that explanation doesn't make sense, I apologize -- it's a complicated issue that's hard to explain... which is exactly my problem with some of the problems I've experienced in Scala :)
Third: The whole "let's encode typeclasses as implicit parameters" thing is insane. AFAIUI this is actually a thing that was intentional. Unless you have actualy real+usable dependent types it's a huge mistake to get rid of coherence[1]. (Plus, TCs are a semantic-level feature. You don't want to force people to "encode" semantic things you care about in your concrete syntax. That way lies "[OOP] patterns".)
Fourth: The whole "for-yield" thing. WTF? Either do monadic notation like it matters or just don't do it. For-yield leads to incredibly noisy code. (It's not even consistent, try starting your for-yield with a "x = ..." line, and you'll see.). The fact that monadic notation starts with a "for" and that collection iteration starts with a "for" is NOT in any way a sign that your language design is "orthogonal" as Odersky loves to claim. It just means that the language designer didn't really understand what he was doing. (As you can probably gather I'm a Haskeller/Idirs'er/etc.). Syntax for these things matters.
Fifth: ... and a follow-up to that: @tailrec isn't enough: That fact that explicit trampolining is basically required for any monadic Scala code basically kills the idea of monadic programming right off the bat... unless you're willing to invest huge amounts of time into it for-yield is just unusable. (Yes, I'm aware of the Free monad. One hopes that the performance problems have been solved, but it's still not always enough to have a Free monad.). Honestly I think it would have been better to invest time into implementing tail-rec on the JVM rather than wasting that time on @tailrec. I do mean wasting -- any situation where @tailrec applies, I could have trivially replaced (or "encoded" that "pattern") that code with a while loop and a var.
Sixth: Why isn't Monad/Monoid/etc. in the standard library? This goes back to my third point which is that the language doesn't actually encode semantics of typeclasses, and so now we have "scalaz" with its Monad and "non" with its Monad, and "fs2" with its scalaz-stripped-down-copy-of Monad. WTF? This is not needed -- these typeclasses have stood the test of time and it's only because Scala insists on encoding them that we ended up here. (Encoding via various implicit magic tricks, the fact that everything has to be an object, etc. It's fucking ridiculous.)
Summary: There's a lot of completely accidental complexity because Scala wasn't really designed as a coherent whole... it just sort of "grew organically". (Like almost every language out there.)
[1] Yes, you gain a "choice" of instances via imports. Guess how often that's actually useful vs. harmful? Never. It's absolutely insane that your imports can change your serialization/deserializion code.
I end up a lot with this;
src/main.rs
src/combobulator/mod.rs
src/combobulator/tests.rs
src/tests.rs
src/somethingelse/tests.rs
src/somethingelse/mod.rs
Because I find tests in the same file a bit confusing. It's really easy with maven-style layouts to know that "only things in main/java or main/scala get compiled and go into the jar". "src/test/*" and "src/main/resources" are for me. The same thing applies for cargo.tomls and resources - there's not really a way to see what goes into the executable from the file structure.But this isn't the biggest problem with having things called "mod.rs". That would be if I open 5 mod.rs's in a text editor with tabs, I have no idea what goes with what.
I know that tests should go under tests/, but that's specifically for integration tests. Integration tests are an order of magnitude less likely to get written imo, and if they are they'll probably get written as unit tests anyway.
If anyone has any top tips for how to structure larger Rust projects while separating unit tests into different files, please let me know!
Also if you keep your tests under "src/", you can't just stick them wherever you want; you need to have #[cfg(tests)] on every module declaration leading to the test function, otherwise the test is ignored silently. From time to time I find old tests that have not ran once because of a missing attribute somewhere upwards the module hierarchy.
src/main.rs
src/combobulator.rs
src/somethingelse.rs #[cfg(test)]
Mod tests {
include!("comboluator_tests.rs");
}
Then write your tests there. It allows you to write unit tests against private things, but also allows you to decouple the files to keep things cleaner.1. Error handling. The lack of built-in support for multi-error or error union in Result is painful in dealing with different types of error in a function. Support for Result<Value, Error1 | Error2 | Error3> would be helpful. Or may be support for easily converting one type of error to another. Now there's lots of boiler plate code to deal with error conversion. Error chaining would be nice, too.
2. Lack of stack trace when an error occurs. Now that stacktrace starts when panic!() is called, which is kind of late.
3. Better support for conversion between &str and String. Dealing with strings is so prevalent in programming that making it easier to work with the two types would be a huge boost to productivity.
Edit: another item
4. Support of partially applied function , i.e. bind a subset of arguments to the function pointer. Currently there's no way to bind the self argument to the Option/Result chaining calls. Basically the Option/Result chain (.and_then, .map, etc) only carries forward the value of Option/Result and nothing else. It would be nice put partially applied function in the chain. e.g. result.and_then(self.func1) where func1 has the self argument bounded. Or in more general form, result.and_then(func1("param1", param2, _)) where func1's first and second parameters have been bounded up front and the value of result will be passed in as the 3rd parameter.
e.g.
`try!(foo().map_err(|e| format!("{}", e.thing)));`
There are a couple of crates that support this; personally, I recommend the "error-chain" crate. However, I do wish that Rust promoted the most capable of those to the standard library.
> 2. Lack of stack trace when an error occurs. Now that stacktrace starts when panic!() is called, which is kind of late.
error-chain provides that.
> 3. Better support for conversion between &str and String. Dealing with strings is so prevalent in programming that making it easier to work with the two types would be a huge boost to productivity.
Can you give some specific examples of cases you've found cumbersome?
String has a Deref instance for &str, so taking a reference to a String automatically works as a &str. You can also call .as_str().
Going in the other direction, you can call .to_string() to make a copy of a &str as a new String.
fn func1() -> Result<MyStruct, Error1 | Error2> {
let foo = foo()?; // which can return Error1
let bar = bar()?; // which can return Error2
...
}
2. Error chain only shows the stack where I explicitly add it to the chain. Anything underneath is not shown. All these should be done by the runtime instead of forcing the developers to add code to handle it.Also, error handling is so prevalent in Rust. If error chain is the way to go, make it as built-in. As right now, everyone has to hit a brick wall with Result, and then hunt around for the same solution.
3. It's the other way around &str to String, having to call .to_string() everywhere. Make it implicit and automatic if the type expects a String while a &str is passed in.
> 3. It's the other way around &str to String, having to call .to_string() everywhere. Make it implicit if the type expects a String while a &str is passed in.
You can't turn a reference like &str into an owned type like String without making a copy, and Rust doesn't do implicit copies (among many other reasons, because doing so would make it harder to notice code patterns that will lead to poor performance). So you'll always have to have some explicit indication that you want to make a copy.
In general, most functions should accept &str parameters rather than String, for exactly that reason; you should rarely run into functions that want a String. You can also use the "Cow" type if you want to support both owned and borrowed strings in the same structure.
Having ad-hoc error union Result per function make it lightweight, and less friction in writing code. I would go one step further, let the compiler build the error union automatically.
fn func1() -> Result<MyStruct, _> {
let foo = foo()?; // might return Error1
let bar = bar()?; // might return Error2
...
}
The compiler infers the list of possible error types returning from the functions. Func1() would automatically have the return signature of Result<MyStruct, Error1 | Error2>.Make the type inference to good use.
Actual global inference has never been on the table and still isn't.
Can you tell me how I would remove the excessive uses of ".to_string()" from the tests in this module? https://github.com/rspeer/rust-nlp-tools/blob/master/languag...
fn main() {
let s1 = "Hello, world"; // &str
let s2 = String::from("Hello, world"); // String
assert_eq!(s1, s2);
assert_eq!(s2, s1);
}
compiles just fine for me?If I change the test value Some("zh".to_string()) into Some("zh"), it points to that line and tells me:
expected struct `std::string::String`, found &str
Sure, it's a different situation because the value is wrapped in an Option. But if String and &str are truly compatible, I would never expect to see that error message.String and &str can normally be compared because of Deref, that is, &String derefs to &str. Option, on the other hand, does not implement Deref, and so no coercion happens. Rust doesn't do a lot of coercions, but Deref is one of the bigger ones.
Back to the _actual_ topic at hand, I can see how this can be a pain point until you know the rules, though. :/
For this particular case, any particular reason we couldn't add an impl of PartialEq? In fact, once we have specialization, couldn't we have a general impl of PartialEq for Options of Deref types?
impl<T> PartialEq<Option<T>> for Option<T> where T: PartialEq<T>
does exist, but I'm not 100% sure why Deref doesn't kick in here, I just know that it doesn't.> couldn't we have a general impl of PartialEq for Options of Deref types?
I think the issue is None. You'd get a null pointer, which doesn't make any sense in safe rust.
No, I don't mean an impl between Option<T> and T, I mean a bidirectional impl of PartialEq between Option<T> and Option<U> where U is T's Deref::Target.
In that case, None doesn't cause an issue; None == None, and Some(t) == Some(u) iff *t == u
-> Result<MyStruct, Box<Error>>
accepts almost any type of error (via dynamic dispatch of things that implement std::error::Error trait).As a beginner in Rust myself, I would like to observe that &str vs. String problems come up all the time for me. Every one of them is quickly resolved by knowing where to add an & or a method call, it seems, and experts know when to do this, and they stop noticing the problem because it's only the beginners who are doing it wrong.
But when every beginner is doing it wrong and every expert isn't, there is an ergonomics problem.
I understand now, but initially it made absolutely no sense that String is a heap-allocated owned string and &str is a reference to a chunk of string data of known length stored somewhere that I probably don't have to care about.
It may be a teaching thing, but I do wonder if they could have been named better. I just don't really know what else you'd call them without them becoming overly verbose.
It sounds like you want to create a new error type that's an enum of Error1, Error2, Error3 and then just implement the From traits. Then you can use ? with no boilerplate error handling.
I'm sure you've already considered this though. Why wouldn't that work for you?
> Often, the heart of the matter is the question of what to make implicit. In the rest of this post, I’ll present a basic framework for thinking about this question, and then apply that framework to three areas of Rust […]
What's proposed here is a universally good way to think about what to make implicit. The proposed changes to Rust are just some applications of this.
I had a completely opposite reaction. It ignores all of the important things that make usability good and instead focuses on the approach that essentially promotes inconsistencies in design.
"The basic thesis of this post is that implicit features should balance these three dimensions. If a feature is large in one of the dimensions, it’s best to strongly limit it in the other two."
The major problem I see is that it's fairly subjective at the moment in what you consider when thinking about those criteria. For example, in the section about eliminating the need for mod (which was admittedly presented as radical), the following was stated: You could instead imagine the filesystem hierarchy directly informing the module system hierarchy. The concerns about limited context and applicability work out pretty much the same way as with Cargo.toml, and the learnability and ergonomic gains are significant. I think this is a case where the learnability would suffer quite a bit. If I understand it correctly, this changes the filesystem from a unidirectional resource to a bidirectional one, where the presence of arbitrary files not specified (as opposed to a well understood singular file, such as Cargo.toml) might change how the code is interpreted.
What would happen, I think, is that adding a `mod foo { ... }` definition would change the behavior of code that previously implicitly referenced a `foo.rs` file. But that seems less crazy to me, since you've got a change in one file affecting something else in that same file. Or it might make sense for that to be a "conflicting module definitions" error.
Can you give some examples?
To me the approach presented here is not just about coming up with an initial design (where I agree there are more aspects to be considered), but iterating on existing design aspects (in this case language syntax and semantics) to make them more useable. Seeing the first dimension the author mentions is 'Applicability' it seems they pay attention to consistency, but I can see it is not explicitly mentioned.
So, what can be made implicit is irrelevant, the proper question would be what can be done to improve user experience. And it's a lot, but the design by committee kind of process is going to work against it.
You might want to vary your kool aid a bit!
The file structure one makes me laugh, because one language that does implicitly create modules from file structure, in exactly the way Rust would need to, is Python, which is the one with the whole "explicit is better than implicit" deal!
[1] https://www.reddit.com/r/rust/comments/5whke7/deref_coercion...
It would be interesting to compare the number of macros defined in the crates corpus divided by total line count and compare that with other languages. I do not think that I am alone in not using it. Yes, I use macros; I just don't program macros.
Obviously, Java has shown that you can survive without a macro pre-processor. That was even a point Gosling+Co made in a white paper I read way back in the day. But I do believe that if you are going to have a macro processor, it should be an expedient. Rust's macro processor is not expedient. It is its own impediment.
I'm used to using macros. I use them in C and I use them in assembly. These are both low level languages which Rust claims to be. Not being able to use Rust's macros in the style to which I've become accustomed is infuriating.
Honestly, I very rarely use macros and have written two in my years of Rust. You almost never need them, or at least, that's my experience.
#define M_PI 3.14159265358979323846264338327950288You wouldn't use a macro for something like that in Rust.
#define PASS_VERBOSE if (flag_verbose && first_pass) printf
you just might be able to but only with a completely different tool.A macro processor is not of the language; it is above the language.
Then feel free to use the C preprocessor with Rust, it works just as well. :P Just like it does with Python, and Java, and...
#define whatever(param) \
foo: \
bar \
baz
I made a preprocessor some 18 years ago that could be used with Python.Wayback Machine:
https://web.archive.org/web/20000815202258/http://users.foot...
It's path::to::wherever::PI. That's it. Just like any other item. If you want to use only PI in your code, you'd use 'use', like any other name.
Your second example is something better suited to a macro, it's true.
I _think_ what you're getting at here is that you only want text substitution? I think we will have to agree to disagree if that's true :)
Yes, I do like textual substitution. Guilty. This is a common old school low level paradigm. Still, the underlying language and its compiler exist below to enforce the rules on any atrocities I commit with my macros.
Those who do use and write them heavily are the ones actually involved in the macros 2.0 effort. There are certainly flaws.
The kind of macros Rust implements, on the other hand, are the kind that have to be language-specific, because they deal with the syntax tree of that particular language. They also enable many things that are outright impossible with text substitution, such as hygienic macros.
These two statements are not consistent. Textual substitution is not usable. The pitfalls with it have been well documented for decades, and yet the same problems persist. Why on earth would you want to perpetuate the list of problems caused by such a facility?
Syntactic macros are absolutely superior to textual substitution. You can take issue with Rust's currently limited support for macros, but to propose textual substitution as a viable and more usable alternative is simply absurd.
const PI: f64 = 3.14159265358979323846264338327950288;
?I'll accept without looking it up that the statement is correct syntax in some language (it doesn't look like C++ to me).
Any time you see a decimal floating-point constant with a nonzero fractional part that doesn't end in '5', you're looking at a bug.
EDIT: As long as this grizzled old Fortran programmer is giving out free advice, I'll add two more items every programmer should know about binary floating-point:
a) Every binary floating-point number can be represented exactly in decimal notation if you use enough digits.
b) Those decimal values are the only ones that can be exactly converted to binary; all of the rest require rounding.
#define PI1 3.141592653589793115997963468544185161590576171875
double pi1 = PI1;
#define PI2 3.14159265358979323846264338327950288
double pi2 = PI2;
assert(pi1 == pi2);
(edit: or even 3.14159265358979323846)
depends on the language. for example here it is in go:
Pi = 3.14159265358979323846264338327950288419716939937510582097494459 // http://oeis.org/A000796So, go might have that value in its source, but it's getting rounded to something that would, if represented in decimal, end in 5.
Having so many digits available means that calculations like Pi/2 or other more intricate evaluations can carry more precision until the result is assigned, making calculations involving constants easier to write without losing precision. It also means that there is no occasion in which the floating-point corner cases like infinities, soft underflows, and NaNs arise in constant expressions.
That's just silly. If you're writing some famous mathematical constant, the digits should match that constant, and not the requirements of the machine. (Except for the last one being rounded off.) Suppose we had a floating-point machine that gave us maximum 4 digits of decimal precision. I wouldn't define the PI constant as 3.145. That would just look like a typo to people who have PI memorized to half a dozen digits or more. I'd make it 3.14159 (or more) and let the darn compiler find the nearest approximation on the floating-point axis.
[1] https://github.com/paavohuhtala/clri/blob/b8a9057397ef95c0de...
let foo = foo()?;
let bar = bar() ? this() : that();expr ? expr : expr is just another form. The ternary form is so well known that I doubt people have problem recognizing it.
foo()?.bar()
(foo()?).bar() let x = if a { b } else { c };
Sure, it costs a few characters, but I appreciate the consistency and clarity.I think a ternary operator would be the first construct in Rust that prevents reading a statement from left to right.
Annotation Processors (iirc Java 1.5) are clearly a form of a pre-processor. It's not macros / textual expansion, though.
Similarly C++ mostly gets along without macros since it contains a capable meta-programming system -- and I think this is the more important point here; for many tasks meta-programming is just a handy thing to have. Dynamic languages don't have that problem, since their runtime is their meta-programming system as well.
Most Java JITs are able to remove code if it is proven unreachable, which allows to use pure Java code for what would be #ifdef in C, with the caveat that all branches must compile.
For example a debug flag can depend on a command line parameter, but it will be used to initialize a constant variable.
So the code can be shipped with both versions, and the JIT will just remove the unused branches.
I feel this exact same way with Go. E.g.
x := map[string]map[string]int{
"key": map[string]int{
"another": 10,
},
}
Given that the outer type signature says that the `value` of the map should be a `map[string]int` it's sometimes quite annoying to specify that inner type over againYou definitely need to specify the type for an interface, but that makes sense.
The facts are that map literals did change a bit in Go 1.5 (i.e. fairly recently), full literal specification for values could already be elided back then - it was KEYS that required full specification: https://golang.org/doc/go1.5#map_literals
If you go way more back, Go 1 also saved some typing on values, in case they were pointers: https://golang.org/doc/go1#literals
but do not forget to document what is implicit. Otherwise, it is magic and make it more confusing. That is the impression of my last attempt to learn rust.
[1] https://www.youtube.com/watch?v=uEFrE6cgVNY [2] http://dl.acm.org/citation.cfm?id=2534973
data Hashable a => Set a = ...
is completely useless. It only forces you to add constraints to functions that, if necessary, would be required anyway.Not to be confused with existential quantification
data ExistentialSet a = forall a . Hashable a => ...
which carries a reference to the hash function in the instances, similar to trait objects in rust.Haskell hasn't found DatatypeContexts very useful, but Rust has. In my opinion this is largely because of the difference in what our type systems mean - in Rust, types carry a lot more information about the memory model & data layout than in Haskell. This has led to a different skew.
In Haskell, DatatypeContexts also create struggles with higher kinded polymorphism (you can't implement Functor for the definition of Set you just provided, for example), but in Rust, the same memory model concerns that make datatype constraints useful make traits like Functor less useful.
By its very nature, Rust is harder (than let's say Python or JS). It is compiled, there's not much runtime magic to rely on and low level is hard.
But thinking about this and trying to make it easier is important
Version 6 introduced an actual AOT compiler to native code.
Also its older brother, QuickBasic, compiled to native code.
Incidently VB is now again being AOT compiled to native code, via .NET Native and CoreRT.
Is there something like rust for c++ programmers?
[0] https://locka99.gitbooks.io/a-guide-to-porting-c-to-rust/con...
There are two main communities in C++, those that embrace safety and take advantage of the language features to improve their productivity, while going down to lower level constructs if performance needs an extra push.
Then there are those that are kind of exiled C developers using a C++ compiler, forced to migrate to C++ on their work, trying to use it as C with C++ compiler.
This is the group that has more issues with Rust.
The thing that keeps getting me now is there are so many types moving around with generics and traits. It would be nice if it were easier for something to be object safe and/or Any was more powerful. My solution as with many things is to route around it but it is frustrating at times.
https://github.com/rust-lang/rfcs/pull/1849 is relevant to your interests; however, as the comments say there, associated type constructors would be needed before it could possibly be used with Any.
I guess compiler plugins and libraries will eventually fill in the gaps, until then I guess I just have to be creative :)
For example in gfx-rs it might be repeating the gfx::Resources or gfx::Factory trait bounds.
The sheer bulk of the code will effectively "force" them to make Rust ergonomic. They might even end up with annoying things, like different sized ints on different CPUs, or... (horror of horrors)... running Rust through a pre-processor as part of its compilation.
The biggest Rust project (which already has many of the core contributors working on it) is Servo, and it's probably about as ambitious as you can get while still having a nonzero chance at completion. I don't think there's much additional value in taking on a completely unfinishable project to shape the Rust language so it's good for a kind of programming (i.e. Kernel programming) that is far removed from what 99% of potential users will use it for.
Hence why he started to design C with Classes after getting his job at AT&T.
https://www.youtube.com/watch?v=ZO0PXYMVGSU
Around 29:00.
https://www.bell-labs.com/usr/dmr/www/chist.html
The best part for those that care about security:
"To encourage people to pay more attention to the official language rules, to detect legal but suspicious constructions, and to help find interface mismatches undetectable with simple mechanisms for separate compilation, Steve Johnson adapted his pcc compiler to produce lint [Johnson 79b], which scanned a set of files and remarked on dubious constructions."
So although C designers saw the dangers of C and provided static analysis from the early days, many C developers keep ignoring them.
Also, Dennis Ritchie developed Unix while simultaneously developing C, so it's not hard to imagine that he added features to the language to simplify his Unix code. At one point he added 100,000 lines to Unix within a year, so he had reasons to make the language ergonomic.
A couple months ago I was exploring Rust's web server capabilities after seeing the "Are we web yet?" page. I decided to try out the `iron` package.[0]
I was quickly able to serve some basic content, but I wanted to add some headers to the response.
I was able to `use iron::headers::Allow` to add an Allow header.[1]
Next, I wanted a Link header. Link wasn't available but I could get around that by defining a custom header with the `header!` macro. Unfortunately, I couldn't figure out how to get the `header!` macro for custom headers without `#[macro_use] extern crate hyper` and adding `hyper` to the Cargo.toml file.[2]
Then I wanted a `Vary` header. I was able to get that in with `use iron::headers::Vary`, but I couldn't actually create one yet! In order to create my `Vary::Items` header, I needed to also `use unicase::UniCase` and add `unicase` to my Cargo.toml.[3][4]
So within an hour or so of starting the project, my explicitly listed dependencies had grown from just `iron` to include two additional dependencies. The iron package already relies upon hyper which already relies upon unicase.
Here are some questions I am still left with. Would love any responses.
Is it possible for me to use the pieces described above without explicitly listing these crates? If not, why do I need to declare hyper as a dependency when iron is already using it? Perhaps I don't need to, and I was just unable to figure out how to get the `header!` macro from iron directly. My initial expectation was that iron would either wrap or expose every part of hyper that I might need. The same goes for hyper not allowing me to just use the same unicase it relies upon.
How am I supposed to get the correct version of hyper and unicase to match with the ones that my version of iron was sent with? Do I have to go look them up? Can use the latest version of `hyper` even if `iron` is a few versions behind? What version should I be specifying?
[0]: http://ironframework.io/doc/iron/
[1]: http://ironframework.io/doc/iron/headers/struct.Allow.html
[2]: https://hyper.rs/hyper/v0.9.9/hyper/macro.header!.html
[3]: http://ironframework.io/doc/iron/headers/enum.Vary.html
[4]: http://ironframework.io/doc/unicase/struct.UniCase.html
Not unless iron exposed them directly. I haven't used it a while, so I'm not sure if it does.
> If not, why do I need to declare hyper as a dependency when iron is already using it?
There's a difference between a direct and transitive dependency, it's not going to just inherently bring transitive stuff into scope.
> Perhaps I don't need to, and I was just unable to figure out how to get the `header!` macro from iron directly.
Yeah I haven't used iron in long enough to say, but this is theoretically possible.
> My initial expectation was that iron would either wrap or expose every part of hyper that I might need.
Yes, it is possible that iron's API isn't the best here.
This is definitely true, and I don't think it's negative. You can use Rust without Cargo and crates.io, but it feels very different. It's nice that that option exists, but I wouldn't want to work that way. Any sizeable project is likely to accumulate a large number of crates.io dependencies, but having written a lot of C++ code, the alternatives are "reimplement this thing myself" or "copy paste some code", neither of which are particularly good.
I wasn't suggesting Cargo being relied upon by everyone was an ergonomics issue, but instead referring to the description in the remainder of my post.
That's a common response to criticisms of Rust. At this late date, that's a problem.
The major rewrite for macros has already brought fruit with Macros 1.1, solving some of the more pressing issues.
The other one, nonlexical lifetimes, needed a complete overhaul of compiler internals. The main part of that work happened with MIR, but there's still work to be done. It's ongoing. This was work that was going to take a long time, and it did.
Right now macros don't quite feel like they are part of the core language, but instead a hack or plugin slapped haphazardly on top of the compiler.
There have been plenty of quality of life features that were promised and delivered successfully. Many new ones have been discovered and are in active, visible progress, but are not implemented yet, so yes, you can always identify some pieces that are still being developed. The amount of resources is finite, but Rust is getting significantly better each year.
Without commenting on the particulars of the iron API (I haven't used it), it makes sense for library authors to be able to make an explicit decision as to whether or not their dependencies are going to be a part of their backwards compatibility contract or not. You could imagine a circumstance where a library A that you used depended on some library B and wanted to switch, internally, to instead depending on library C. If library A had exposed library B to its consumers as part of a public interface, that would be a breaking change, but it wouldn't be if they hadn't.
Absolutely. But as soon as those are required in order to use your library, shouldn't they be included?
> If library A had exposed library B to its consumers as part of a public interface, that would be a breaking change, but it wouldn't be if they hadn't.
Maybe I should have refined my original post. I guess it was way too long.
The hyper crate exposes `hyper::header::Vary`. To create a `Vary::Items` in my program, I also need to `use unicase::UniCase`. See the short example at [0].
It felt odd to me that I was required to have my own dependency on unicase when it seems like an internal issue for how Hyper represents the Vary header.
I have little experience with these dependency management systems. This seems like an irrelevant detail (they look just like strings to me! why doesn't it just take strings!) requiring me to explicitly include an unrelated package. I may have no other need for the unicase package.
Is this a normal thing, though? Since unicase is required in order to use this feature of hyper, shouldn't it just come along with it? Or the Vary item could just use Strings on its public interface so the user doesn't have to go through this extra work?
[0]: https://hyper.rs/hyper/v0.8.1/hyper/header/enum.Vary.html#ex...
I'm a statically-typed, "easy-on-the-eyes" Python looking-loving guy, but there's something beyond that, when it comes to "ergonomics".
I'm really impressed with the Clojure community and parinfer, paredit(old lisp school) and just slinging code around with rapid feedback.
If they had fields, what would you do when you implemented two of them on a struct? It also muddles up the ability to tack on traits in later crates.
Traits aren't supposed to be used the way you do single inheritance. Rust prefers composition over inheritance, so you combine structs and enums to get what you want there.
The design isn't done yet: https://github.com/rust-lang/rfcs/pull/1546
That RFC lets you declare fields that the trait implementor is supposed to provide; implementing a trait will not automatically add a field, instead, you will be forced to add such a field yourself (unless one already exists) and link it to the trait field.
It's a more rusty way of addressing many of the same use cases, but it's not the same thing. It's basically sugar for declaring a getter and setter as trait/interface methods.
When you create a class in an OOP language, you have do declare all interfaces it supports, and provide implementations for them (or mark the class as abstract). In Rust's case, you declare the data separately as a struct declaration, and then implement the traits you want in impl declarations. You can also implement traits for other traits, or for types from other modules, including the standard library.
Finally, compared to traditional OOP interfaces Rust's traits can require static functions in addition to instance methods and they can also implement methods with an overridable default implementation.
[1] https://en.wikipedia.org/wiki/Trait_(computer_programming) [2] https://en.wikipedia.org/wiki/Type_class
With specialization, instead of inheriting classes, traits can provide blanket and default implementations for types that depend on the traits implemented by that type. So, for example, you can provide several different "impl<T> ExTrait for T" with different trait bounds like "T: Clone", "T: Clone + Send", and "T: Clone + Send + Future<...>". Once the conservative inference algorithm is improved, this will be a much more powerful way to compose functionality than OOP but it will require a different approach to abstraction.
I think the plan is to eventually add trait objects that implement multiple traits which will cover more complex cases of dynamic dispatch like those in virtual inheritance.
Rust also has Trait, which is like interface, with a set of method function signatures. A Trait is implemented for a Struct to have concrete method implementation. Trait supports inheritance.
pub fn do_many(iter: &mut Iterator<Item=String>) {
let mut job_id = None;
let job_id_env = env::var("MYAPP_JOB_ID");
let mut log = if let Ok(val) = job_id_env {
write_pid_file(&val);
job_id = Some(val.clone());
let home = env::var("HOME").expect("HOME must be set");
let path = format!("{}/log/myapp-{}.log", home, val);
let path = Path::new(&path);
match File::create(&path) {
Ok(mut f) => Box::new(f) as Box<Write>,
Err(e) => {
if format!("{}", e) == "No such file or directory (os error 2)" {
Box::new(io::stdout()) as Box<Write> // oh well
} else {
panic!("Can't open log file: {}", e);
}
},
}
} else {
Box::new(io::stdout()) as Box<Write>
};
// Commit the tx if we get these signals:
let signal = chan_signal::notify(&[Signal::INT, Signal::TERM]);
let negotiator = OpenSsl::new().unwrap();
let url = env::var("MYAPP_DATABASE").unwrap_or("postgres://myapp_test:secret@localhost:5432/myapp_test".to_owned());
let tls = if url.contains("@localhost") { TlsMode::None }
else { TlsMode::Require(&negotiator) };
let conn = Connection::connect(url, tls).expect("Can't connect to Postgres");
let db = make_db_connection(&conn); // defines a bunch of prepared statements
// now we can do stuff . . .
}
I would really like to have just this: let log = open_log();
let db = prepare_db();
But those don't work, because all the temporary values are going to fall off the stack when the helper functions return. I wish rust were smart enough to make the functions put the values directly in the caller's stack frame. Alternately, I wish rust would let me say that all those temporary values should live as long as the returned thing (log and db), so it can keep them around even if I don't have variables for them.I thought maybe macros would help here, since there is no new stack frame, but they still introduce a new scope that limits the lifetime of the temporary variables.
Even worse, if I want to write tests for functions that use the log and db, I need to repeat all that code again and again.
I think the answer is to use Box here? I haven't worked that out yet, but it definitely feels harder than it should. And even if I can make it work, I'm a little sad that I have to give up stack-based allocation.
I've also read that the answer might be OwningRef (https://crates.io/crates/owning_ref), but I'm not sure yet. I wish the Rust book had a section about it. It seems like Cow and Rc might also help me---I don't think so, but I'm not positive yet. Covering these allocation-related crates in a systematic way would be nice.
Anyway, I'm just a Rust newbie, but it sounds like the ergonomics effort is (partly) for newbies like me, so I'm trying to express my struggles in terms of a pattern that the Rust team could optimize for. It seems like something that people would hit quite often. I'm sure there is an answer to what I'm trying to do, so my point is that maybe it should be easier to find, or at least better documented.
But in general, if you have a routine that is creating stuff, and the stuff is meant for the caller to use, you create it in the routine and return it by value; the caller will then automatically "own" that value and either pass it somewhere else, or let it fall out of scope (which is when it'll be dropped and cleaned up for you).
That's one reason why macros exist.
> I thought maybe macros would help here, since there is no new stack frame, but they still introduce a new scope that limits the lifetime of the temporary variables.
Why can't you just "return" the variables you need later on ?
Possibly I've missed something critical about your example, but I think you may want to create a struct Log, turn open_log() into Log::new(), and put the things the log needs (such as the log file) inside Log, owned by Log.
I will try your suggestion re the log. The database example is trickier I think since the prepared statements have references to `conn`, so it can't move. Also it's annoying that I have to make `negotiator` even when I don't need it.
Manually collect the things you need to hold onto and put them into a struct and return that from open_log(); Do the same with prepare_db(). Then give the structs some methods for getting to the actual db object.
Alternatively use log4rs and rust-postgres. Or inspect their code to see how they handle it.
return true if number > 2;
VS: if number > 2 {
return true
}
I find that one-liners like these are really ergonomic.``` fn foo(number: u32) -> bool { if number > 2 { true } else { false } } ```
I mean obviously in this situation you could just have the body of the function be `number > 2` but I write a lot of Rust code that does similar things now.
sub compute_interest( $amount, $interest ) {
return $amount if $interest == 0; # Quick return
die "We don't allow computation of negative interest rates" if $interest < 0; # Throw an exception
# Do the actual work
...
}
Edit: Also, it's worth noting that Perl enforces some behavior on this by only allowing postfix conditionals to follow a single statement, not blocks, so it's not just a regular conditional with the order reversed.Other than that, early returns can simplify flow a lot - otherwise you may have to do massive nesting ifs or many flags. Or even goto or exceptions.
When used with a return or die (or my personal favorite for debugging with 'warn "FOO" if $ENV{DEBUG};"), the fact that flow is affected is obvious by the very first characters in the statement, so it's obvious to then look for when it applies.
Like so many features of languages, how it looks from the outside compared to how it looks from those that are well versed in the language can be quite different (not to say that everything that looks like a wart in Perl is okay once you get used to it, every language has real warts). That's another aspect to this whole thing, how much to you emphasize ergonomics that are primarily for learning and novices. Features focused at novices to the expense of those familiar with the language are interesting, because they may draw a lot of people to your language, but you may not retain them very well.
If it's the last expression it will implicitly return the values. Easily fits on one line.
In both languages you can do the following:
return number > 2; x = 42 if y > 7;
Rather than x = match y > 7 { true => 42, false => 0 };
Or even: foo() if bar();
Rather than: if bar() { foo() } return if($number > 2);
Is equivalent to: if($number > 2){
return;
} let x = if number > 2 { "yes" } else { "no" };
It's not quite the same, and I'm not sure what the failure case would be if the 'else' statement was ommitted, but it comes close.After experimenting with Rust and Elixir I've really come to like the 'everything is an expression' approach. That and pattern matching can make some things really expressive.