I think this is a very important question. Rust is certainly betting
that programmers are willing to learn new concepts and tools. I don't
think anyone will be able to say for certain if this bet will pay off.
Any time that we talk about introducing a new language feature, though,
you can be sure that we worry about exceeding our complexity budget.
I want to look again at this example that you raised. However, I've
modified by removing the `let` statements that are there just to
workaround a bug. Itlooks a lot simpler now, but there are still a fair
number of concepts at play here.
pure fn each(&self, f: fn(&(&self/K,&self/V)) -> bool) {
match *self {
Leaf => (),
Tree(_, ref left, ref key, ref maybe_value, ref right) => {
left.each(f);
match *maybe_value {
Some(ref value) => { f(&(key, value)); }
None => {}
}
right.each(f);
}
}
}
Here as I see it are the important new ideas:
- closures.
- matching against disjoint unions and `ref` bindings that create
pointers into the structure being matched.
- lifetime declarations in the signature, tying the lifetime of the
key/value pointers that will be provided to the lifetime of the receiver.
The first two seem to me to be clearly within your average C++
programmer's range of understanding. (Let's not forget that to write C++
programs that actually work without crashing—as opposed to just ones
that compile—you've got to have a fair amount of knowledge to start
with). The third point, lifetimes, is where things get more complex.
Certainly C++ programmers have an intution for this, but it's never been
necessary to notate it before.
There are some mitigating circumstances. For example, the signature
could be modified to drop all explicit lifetimes, as follows:
pure fn each(&self, f: fn(&(&K,&V)) -> bool) {
This would still typecheck. However, it would be providing less
information to the iteratee. That is, the function `f` is only being
told to expect two borrowed pointers, but it is not being told what the
lifetimes of those pointers are. Therefore, it must assume they are only
valid for the duration of the call. This is usually good enough but not
always, so it's certainly better for libraries to go the extra mile in
terms of providing full annotation.
Anyway, I'm not precisely answering your question, of course. I don't
know the answer. I think though that, if you want to guarantee safety
conditions, there is a big danger in designing the language to be *too*
simple. I think a lot of things that seem simple at first wind up being
frustrating once you've gained more experience. We definitely want Rust
to be as simple as possible, but not so simple that it can't express the
kinds of things you want to express.
For example, the by-reference modes that predated lifetimes were quite
possibly easier to understand (though I'm not sure, let's not forget
that they engendered a fair amount of confusion, even amongst the core
developers), but they were also fairly inexpressive. I ran up against
these limits *all the time* in practice and the only escape was to make
use of managed pointers. That was part of the motivation for regions in
the first place.
That said, you don't want the language to be too intimidating. I worry
about the idea of a subset that we encourage people to stick to, but I
do think we should think about the order in which ideas should be
"phased in". I hope also that when writing casual code people can get
away with less specific annotation.
Niko
Dean Thompson wrote:
I am new to Rust, but quite excited about it. I have read most of the
docs
carefully.
I'm looking at some code that Niko Matsakis updated in
https://github.com/stevej/rustled/commits/master/red_black_tree.rs
pure fn each(&self, f: fn(&(&self/K,&self/V)) -> bool) {
match *self {
Leaf => (),
Tree(_, ref left, ref key, ref maybe_value, ref right) => {
let left:&self/@RBMap<K,V> = left;
let key:&self/K = key;
let maybe_value:&self/Option<V> = maybe_value;
let right:&self/@RBMap<K,V> = right;
left.each(f);
match *maybe_value {
Some(ref value) => {
let value:&self/V = value;
f(&(key, value));
}
None => ()
};
right.each(f);
}
}
}
I understand this code reasonably well. I greatly value the attention
to safety in Rust, and I appreciate the value of pointer lifetimes in
maintaining that safety.
My gut reaction, though, is that this code is almost as intimidating
as Haskell. Even more worrisome to me, I think most mainstream
programmers would find the *explanation* of this code intimidating.
Who is our target audience for Rust? Graydon has said it is
"frustrated C++ developers", but how sophisticated and how "brave"
are we thinking they will be?
(I'd like to think of myself as a team member who is just getting
started, so while deferring to the senior folks, I'll say "we".)
How intimidating do we think Rust is today? Am I just overreacting
to unfamiliarity?
How can we calibrate our "intimidation factor" before language
decisions start getting harder to change?
Do we want (and is it feasible) to define a simpler subset of the
language that beginners are encouraged to stick to and that most
libraries don't force clients away from?
Dean
----
Dean Thompson
https://github.com/deansher
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev