Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
My experience rewriting Enjarify in Rust (medium.com/robertgrosse)
230 points by buster on Sept 19, 2016 | hide | past | favorite | 121 comments


Good write up.

I can agree with him on a couple of points.

I've found Rust code to become terribly verbose and unreadable for certain tasks. You have to be careful to not end up with messy and hard to read code. Lifetimes of course make it even worse. Some common patterns are hard / awkward to implement. The stdlib is (intentionally) very small, and throwing together various crates adds a considerable maintenance overhead. Overall I find Rust really promising though. It might need a few more years to mature.

I totally agree on Go: I intially really loved it and used it all the time. The more code, and especially the more reusable libraries I wrote, I started hating it more and more, to the point where I only touch Go if I have to, or for small, one-off, self contained projects that need a bit more perf than Python or where Haskell is not appropriate (team, problem space).

The only thing that's a plus for go is it's dead-easy concurrency.


I've not looked at Rust, but I've been using Go in production now for years at this point with non-trivial code bases. In our cases, they are mostly SMTP and HTTP servers. Sometimes the boiler plate gets in the way (and I think Go should almost never be used for simple CRUD stuff, there are better options), but I find the readability fantastic. It really dawned on my how readable it can be when I had to fix a missed condition in clean up on shutdown. It was fixed in an hour. For the perl version that the Go one replaced, it would have been a day before I even understood what would have needed fixing.


Really not a property of Go. Any codebase can be hard to follow if the designer wants it to be. It just happened that the Perl community went off the deep end and decided that tiny unreadable code was better than big readable code.


> It just happened that the Perl community went off the deep end and decided that tiny unreadable code was better than big readable code.

That's one way to look at it. The other is that they embedded more syntax which makes the learning curve longer, but yields more more efficient writing and reading of code once you've fully internalized that syntax. The big mistake, IMO, was that they also made the language easily usable to get something done before you've fully internalized those rules, which leads to subtly wrong or weird programs that just work most of the time.

Perl is fundamentally different than most languages in how it uses context, but you can get away with not really understanding that at all by just learning a few rules-of-thumb. This, predictably, results in frustration in the writer, as they run up against what look like rough edges and weird idiosyncrasies in the language but are often just places where they've never actually learned how Perl really works.

That's not to say Perl doesn't have actual rough points and problems (what language doesn't after almost 30 years?), but contrary to popular belief, Perl is actually highly designed, and usually conforms to a few core principles (even it those principles are not what many other languages emphasize).


It's worth keeping in mind that Perl, like its predecessors sed and awk, was intended among other things to be useful and convenient as an embedded language in shell scripts.


> I think Go should almost never be used for simple CRUD stuff

Why?

> there are better options

Which ones?


PHP, Ruby, Javascript, Python - pick one.


Why are they better? Dynamic languages are orthogonal to creating CRUD apps and come with massive runtime and reading overhead.


The overhead doesn't matter for simple CRUD stuff. The developer's time is what matters. The dynamic language are more productive.


> The dynamic language are more productive.

Do you have a source for that? I hear it a lot and I just don't buy it.


If dynamic languages require less code and less hoops to jump through with more high level abstractions available to express yourself with, then chances are they will be more productive. There is a price to pay for this expressivity in terms of performance, overhead and potential bugs not caught at compile time. This is well understood, which is why you hear it a lot.

Now like most things having to do with programming, there is little empirical research backing up any language claim. As such, your anecdotal experiences might be different than the what you hear a lot. Maybe you are more productive in a statically compiled language. Maybe the metaprogramming features in a Ruby or Lisp don't make you more productive. Perhaps a more restrictive language fits your coding style and thought processes better.

Or maybe you're a Haskell programmer, at which point I would defer.


There are basically not many conclusive and reliable studies out there that would show empirical evidence for or against dynamic or static languages (software engineering is not even a social science in this respect, i.e. psychology and sociology are more empiric than us).

From my personal and anecdotal evidence, I would say that dynamical languages are on average a good choice in terms of productivity. More or less, Smalltalk and Lisp have explored that design space and we have a fairly solid understanding of what a good dynamic language looks like (basically, most of them really look all the same if you look beyond the syntax). There are established practices and resources. I would even go so far and say that with dynamical languages - even though they are highly flexible - we have a pretty homogeneous situation. The most important differences between let's say Ruby, Python, Javascript and Clojure is in the ecosystem (libraries, etc). So I say: So dynamic languages are this robust and reliable family van.

With statically typed languages the situation is not so homogeneous. The selling point of statically-typed languages is that they can give you compile-time guarantees. Yet we have expressive and non expressive type systems. Languages that only allow for simpler types (golang), for complex types (ML/Haskell, and Rust which is simpler in terms of polymorphism yet more complex because it has these lifetimes). What I am trying to say is that already when speaking about expressivity, there are differences in statiically typed languages that are pretty huge. Yet they don't map 1:1 to productivity. I don't think there is a language better suited for compiler-writing than Haskell, yet it will be an unproductive language when I use it as a replacement for shell-scripts (or Python scripts that do shell script tasks). And I frankly don't know if Golang would be the right tool for this (if I had the choice between Golang and Python for writing a compiler ,I would probably choose Python).

When it comes to a CRUD application, it probably is not so much a question of which language you pick for implementing it, but for how well you understand the patterns (MVC, etc) established for CRUD applications, how nice the ecosystem supports you (libraries, frameworks, etc) and how well you write your tests.


Isn't it more of an opinion than an objective truth? ie, it doesn't really need a source.


That is what I was angling toward.


> The only thing that's a plus for go is it's dead-easy concurrency.

Since you mention Haskell, I would like to say that Haskell's concurrency support is more diverse and at least as easy as Go's.


> I would like to say that Haskell's concurrency support is more diverse and at least as easy as Go's.

More diverse is easy, Go is opinionated about its concurrency.

"At least as easy" is a flaming lie. The diversity alone makes it impossible but beyond that:

* when you look for concurrent go you're introduced to 3 constructs (`go`, channels and `select`) and you're off to the race having barely learned the language

* when you look for concurrent haskell you're directed towards the corresponding paper, "tackling the awkward squad" and a world of Async, MVar, ThreadId, STM and various types of Chan, and at that point you still can't write a line of concurrent haskell

Like the_duke I don't enjoy Go, but with respect to dead-easy concurrency the only language that's in the same ballpark is Erlang (and it's similarly boiled down to a complete lack of diversity — though a different model than Go's — and three basic constructs: `spawn`, `receive` and `!`).


> * when you look for concurrent haskell you're directed towards the corresponding paper, "tackling the awkward squad" and a world of Async, MVar, ThreadId, STM and various types of Chan, and at that point you still can't write a line of concurrent haskell

I was directed here: http://community.haskell.org/~simonmar/pcph/

And after reading the first few chapters, I'd place go's options under the Rich Hickey quote of simple versus easy. Additionally I sped up a simulated annealing program with the accelerate framework in Haskell by typing "Acc " twice. From 1 000 iterations/minute to about 500 000. I'd call that an easy win. Haskell also allows for more than simply concurrent programming. I always ended up with locks in go and wondering why I was using it over c with an event loop and threads at that point.


Haskell's concurrency advantage is that the answer to the question "Does Haskell have X concurrency construct?" is "yes", and additionally, it's generally safe to use because everything is immutable. It even has support for things that have proved difficult or impossible in almost every other context because of that immutability, most notably STM.

Haskell's concurrency disadvantage is the same set of answers to the same question. It intrinsically has a big toolbox to start with, and a bigger one once you get into the libraries that implement further things on top of it (the pipe libraries, for instance), and that intrinsically makes it harder.

So, for instance, yes, you certainly sped up your program very easily, but new users often have a hard time finding their way to the best resources, and if their problem is not a trivially parallelizable algorithm, which it sounds like you had, there can still be some trouble in figuring out which concurrency construct they need.


STM is useless with full immutability: it is safe in Haskell because effects (including mutation) can be controlled/packaged into contained areas, via abstractions like monads.

That said, one downside of STM is that it isn't as efficient as some other schemes especially with high contention, as everything gets serialized through the metadata for tracking transactions. It's an awesome tool, but not a panacea (like with any other technology).

(I'm sure you know this, but others reading the thread may not.)


Yeah, also the STM support is often overlooked and is a good fit for some tasks where everything else is much harder.


You can't even write a generic pmap in go. Right there haskell wins.


> If you want to be able to refer to the contents of the enum, have named fields, or call methods, you need to create a separate struct definition.

Not so! An enum variant may contain named fields. From The Book, https://doc.rust-lang.org/book/enums.html :

    enum Message {
        Quit,
        ChangeColor(i32, i32, i32),
        Move { x: i32, y: i32 },
        Write(String),
    }


I agree with him though that enum variants not being distinct types is a hassle.

I think there's an RFC for making this possible.


This is one of my last remaining nitpicks with Rust as a language. In Scala, sum types are a form of sub-type polymorphism, where each variant is its own distinct type and can be used as such. To get similar behavior in Rust, you end up having to wrap structs in the variant like this:

    struct Foo_
    enum Bar { Foo(Foo_) }
It works, but it is extremely annoying.


It's by design AIUI; certainly it's often what you want (I won't claim "always", but probably more often than actually wanting the distinct subtypes).


Even Haskell needs an explicit language extension flag to make this happen (-XDataKinds I believe).

Consider this use case

    data Pointer = NullPointer | NonNullPointer Int
And I want a function that only accepts non-null pointers:

    dereference :: NonNullPointer -> a
To do that and maintain my Pointer type, I'd have to do something like:

    data NonNullPointer = NonNullPointer_ Int

    data Pointer = NullPointer | NonNullPointer NonNullPointer

    --                           ^ The namespace of constructors and types is distinct,
    --                             so this is fine and is a constructor of Pointer that
    --                             contains a NonNullPointer, which only
    --                             has one constructor, NonNullPointer_ (note the _)
I believe -XDataKinds does something like this behind the scenes, but someone better than I should verify. I just use the above technique rather than questionable language extensions.


DataKinds lets you use NullPointer and all the various NotNullPointer values as types, but they are uninhabited. It doesn't (obviously) let you restrict a parameter to values using a particular constructor of a sum type.


Just looked it up and it was GADTs I was thinking of that would allow me to do something like that.


Ah, yes.


I'm always glad to get feedback, we're specifically going to focus on ergonomics and smoothing rough edges this year, so this kind of thing is invaluable.

There's some good discussion on the Rust subreddit: https://www.reddit.com/r/rust/comments/53ft4m/my_experience_...


Off-topic, but since you're here, let me ask: where can I track the progress on rustup support for musl-based distros where musl is the host (void linux, alpine linux) and FreeBSD? I got everything but Rust working nicely on void linux, and it's the only thing holding me back right now. Since Docker defaults to alpine linux, the reach of such support will be wider than one might initially think.


For void: https://github.com/voidlinux/void-packages/issues/4570

And alpine: http://pkgs.alpinelinux.org/packages?name=rust*&branch=&repo...

https://github.com/rust-lang/rust/issues/31322#issuecomment-... is the latest news on the subject. It's not impossible, but we're not sure _how_ we want to make it easy. The real issue is the bootstrapping, my understanding is that cross-compiling a musl compiler from a gnu compiler does work today, which is how those packages were made.


Thanks for the pointers. Yeah, after you've bootstrapped once, you have a version to bootstrap another, especially since the new requirements to not depend on a nightly snapshot. rustup should be able to provide images that way which work on both alpine and void. The default would be a dynamically linked against musl rustup image which may still expose musl-static as a target just like it does on glibc hosts. I wouldn't complicate matters as a first step with questions of whether a musl image should be statically linked.


I tried rustup.rs on PacBSD, and everything just worked, so presumably it'll just work on FreeBSD too.


Great that you're open to feedback! I'm looking at this snippet from the guessing game in the official Rust documentation:

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");
There are two things wrong here: 1) I have to use mutability to read from standard input. 2) I have to use a pointer. How about something like this instead:

    let guess = io::stdin().read_line()
        .expect("Failed to read line");
Make it a macro or something if necessary. Personally, I just looked at this and decided that Rust wasn't ready for use by non-low-level programmers.


The API used to be what you suggest, but it was changed because it forces doing a new allocation for each line. By taking a mutable string, code can reuse the backing storage of a string (or append to an existing one), which is especially useful for efficient loops.

> Personally, I just looked at this and decided that Rust wasn't ready for use by non-low-level programmers.

That's definitely a valid sentiment, but I think a better framing would be "isn't ready for use in non-low-level contexts": Rust is (and aims to become even better) an enabling technology that allows higher level programmers to more easily write performance sensitive code, because the aggressive compiler checks help people sidestep days/weeks of subtle pointer pain. Of course, the language focuses on this low-level/performance-sensitive space, and other languages may make more sense if those benefits aren't important for your particular use case.


I'm not saying that the possibility of doing it the current way should be removed, I'm just saying that there should be a cleaner/easier way that's recommended in the Rust docs. If people really need to optimize this that's fine, but it should be opt-in, not the only way to do it.


> If people really need to optimize this that's fine, but it should be opt-in, not the only way to do it.

Be careful, you're taking goals of other languages and layering them on Rust. It's entirely valid for a language to decide that the default idioms should not be sub optimal where that's easily achievable. In a language meant to compete with C/C++ on performance, defaulting to a slower idiom when security is not in question may not be the correct choice. In a language that is accepting some level of non-negligible performance loss in idiomatic code already (such as Go), the decision may be different.


Valid point. My perception is that Rust wants to be a little more mainstream than C++ though.

That said, my conclusion after looking at Rust is that it's Haskell for compile-time memory management; i.e. it won't become very popular, but it'll influence languages that do.


I'm not entirely sure what you mean by "a little more mainstream", which I read as implying C++ isn't mainstream, and when I think mainstream programming languages the top two in my mind, when generalizing across domains, are C/C++ and Java. There are specific areas where they aren't somewhere very close to the top (webapps, but even there Java makes a showing in the enterprise), but those are the exceptions.

My impression of Rust has always been that it's trying to supplant C++, by providing something with similar performance characteristics while being safer and with somewhat improved ergonomics (given the required complexity increase for the additional security, which may yield a net gain or loss in ergonomics depending on your point of view and background).

> it won't become very popular, but it'll influence languages that do

That's entirely possible, and I'm willing to accept that as a good thing if it happens. :)


It's a shame there aren't more "Modula" derived modern languages, rather than trying to force C into being a higher level language.

Java, especially at the 1.1 mark, felt like some kind of crippled version of Borland's Pascal/Delphi + a UCSD P-system VM (dressed up to look like C++).

Microsoft for C# hired the language designer from Borland.

The Go team includes a Uni of Zurich guy, and at least decided to use a declaration syntax (when variable types aren't inferred) more like Pascal.

Start with a language that wasn't a shiv with a broken handle in the first place to build applications.

It's a shame Eiffel never took off instead of the wretched C++, no, this time for sure, Java... (not, that's not it, either - lather, rinse, repeat) lineage.

(C was vastly preferable to assembly in the late 80s / early 90s, but it's time to let it go, and its syntax/semantics, people)

Irony: I like the unix philosophy of composing processes in a functional pipeline, which is totally lost in the era of monolithic java crud, but that is another huge tangent.

RANT ENDS.


The Rust community is pretty good at turning specific complaints into actionable proposals. Could you perhaps be more specific about what nice things in Modula/Delphi/Eiffel are missing from other languages, or about what might be different if Go or Rust borrowed more of their influence?


Thanks for the response, it was just a rant.

I suspect the Rust community and I simply have irreconcilable differences. They want a better C or C++, and like special character ditties such as &, *, <_> (whatever that does), and I would rather have short keywords in some places.

For example, declaring a parameter with "var" in a function heading means it is a reference, instead of a copy. However, there is no dereferencing syntax, you simply use it as if it were a local "auto"/stack type variable (which also cannot be a null, so no checks are required - which Rust probably takes care of as well if you ask nicely), and there is nothing special for the caller to do, unless the caller has a pointer that needs to be checked and then dereferenced in the call.

I also miss reasonable "set" operations, even if the sets were just bit vectors on short enumerated types (enums or short sub-ranges of other discrete values). Maybe Rust has sane set operations, but Java's collections operations are a bit off: mutating "add-all" vs union producing a new set, and so on.

Eiffel's pre/post-condition and class invariant special assertions are nice, as well.


Rust's set types do implement the BitAnd and BitOr traits, which allow using the & operator for intersection, | for union (There's also a few others, like (Sub, -, set difference) and (BitXor, ^, symmetric difference)).

They use Clone to construct a new set for the result, for use with a functional style. There are also the union/intersection methods that produce lazy iterators over references to the values in the sets, for cases where cloning the set elements would be too big a runtime price, at a slight cost to ergonomics.

Clone-based operator overloading:

https://doc.rust-lang.org/std/collections/struct.HashSet.htm...

https://doc.rust-lang.org/std/collections/struct.HashSet.htm...

Lazy-iterator-based methods:

https://doc.rust-lang.org/std/collections/struct.HashSet.htm...

https://doc.rust-lang.org/std/collections/struct.HashSet.htm...

Do these qualify as reasonable set operations?


<_> is for when you want to specify the outer type of a generic, but don't feel like explaining the inside. So if something could infer to a Vec<i32> or a Set<i32>, instead you can say Vec<_> and have it infer the inner type.


Based on this would be nice to know things that still can be used within Rust :)


This was exceptionally well written, and it closely mirrors my experience as a python programmer slowly moving into Rust.

Rust is awesome, but there are a few rough corners where you end up having to write really un-ergonomic code.


After reading this article my conclusion is that Rust is some kind of static-typed Perl with a focus on memory safety.

The syntax is horrid.

steveklabnik asked me once why I thought Rust is more complex than C++. Well, if one takes a look at any of the Rust snippets in this article without being deeply disturbed by the @'s the .'s, &'s and *'s, then we have a completely different idea about what it means to build a user-friendly and ergonomic programming language.

Luckily I don't have to pick between Rust and Go, but if I had to I would just stick to Python and use type annotations after reading this.


In a systems language, it's important to know if you're passing around things by value or reference. We follow the Python maxim here: explicit is better than implicit. This means &s to indicate where something is passed by a pointer, and * for dereferencing. This is the same as C++, we just do less magic coercions. We also tried to not make up too much syntax, and stick to "curly braces and semicolons" for most things. Even lifetime syntax isn't new, though OCaml uses it for a very different purpose than we do. (There also aren't any @s in there, unless I missed something? And . is the same as in any language: for method calls, and we made it _simpler_ than C or C++ here, by having . instead of . and ->.)

I agree that you can make a language without many syntactical constructs, but Rust has other goals as well. Language design is hard! There's tons of tension between various choices.


Rust is exceptionally noisy and terse. Easily worse than C++, in my opinion, which is known for being noisy.

Two things bug me in particular. Rust eschews readability by erring on the side of terseness — fn instead of something like func, for example, might save some typing, but does contribute to making visually denser.

The choice of snake_case instead of camelCase also adds to the noisiness, because there's no programmer font that aligns the underscore with the text baseline, so it's disruptive. Snake case is pretty aptly named; typographically, there's a lot more visual cohesiveness to camelcase. (As an aside, I never understood the lack of consistency here: the capitalized version of foo_bar should surely be Foo_Bar, not FooBar.) Python admittedly uses snakecase, but avoids looking too ramshackle by having otherwise very little noise.

Compare something in Rust (from OP's repo):

    fn type_list<'a>(dex: &'a DexFile<'a>,
      off: u32, parse_cls_desc: bool) -> Vec<&'a bstr> {
to Go:

    func typeList(dex *DexFile,
      off uint32, parseClassDesc bool) []BStr {
I'm not the world's biggest fan of Go, but they did nail the readability aspect. Nim is another good example which does it even better.

Of course, Go doesn't have lifetime annotations, but I can't understand who thought using ' was a great idea.

I wish more time was spent on the visual aspect of a language. I'm really liking Reason, Facebook's new syntax for OCaml, which shows that a language can be made fresh and friendlier through aesthetics alone. Elixir is cleaner than Erlang, but I think it went too far in emulating Ruby; instead of very simply aligning "def" and "end", for example, you now have "do ... end" everywhere).

Edit: Typo.


Your criticism comes down to wanting a different abbreviation for "function" and wanting camel case instead of snake case. I would suggest that it's not that we didn't spend time on the "visual aspect" of a language: rather it's that we made choices that were not your preference. We had to pick something and disappoint somebody; I apologize that that person was you.

> Compare something in Rust (from OP's repo):

As with so many of these comparisons, you're effectively comparing a language with GC to a language with safe manual memory management, and misinterpreting this as evidence of bad syntactic choices. (Same with Nim: Nim has GC.)

Look at the function signature when you remove the lifetime annotations:

    fn type_list(dex: &DexFile, off: u32, parse_class_desc: bool) -> Vec<bstr>
When you do that, there's very little difference between that and Go.

> Of course, Go doesn't have lifetime annotations, but I understand who thought using ' was a great idea.

Me. It was chosen because it's very visually lightweight.


> Your criticism comes down to ...

No, those were only the two most obvious, easiest-to-solve examples that came to mind.

With all due respect to you and your Rust colleagues (who have done a lot of impressive work), "we made choices that were not your preference" sounds a little condescending. These are my personal opinions, just as your counterarguments are opinions. We should all able to make technical arguments without resorting to that kind of rhetoric. Of course you made choices. Everyone here is entitled to disagree with them and explain why.

There's a false dichotomy between "hard-to-read syntax" and "no syntax at all" being implied here. It's reasonable to imagine that Rust could have had less terse, but still readable, syntax, even for things like lifetimes.


> We should all able to make technical arguments without resorting to that kind of rhetoric.

The subtext here is that these decisions aren't technical. They're pretty arbitrary: "func" vs. "fn" or camel case vs. snake case. There's no science here: there are lots of very successful languages with camel case and lots of very successful languages with snake case.

I certainly appreciate that you have specific reasons for your preferences, but I take issue with the idea that we didn't spend time on the visual aspect of the language.

> It's reasonable to imagine that Rust could have had less terse, but still readable, syntax, even for things like lifetimes.

Do you have a proposal?

There were long threads with different proposals back when this stuff was being designed, but none of them were particularly palatable. The verbosity with anything that wasn't as lightweight as possible turned out to be too much.


you're effectively comparing a language with GC to a language with safe manual memory management, and misinterpreting this as evidence of bad syntactic choices.

So a better comparison would be to Swift. I don't think Rust compares favorably with Swift either.


Swift doesn't have lifetimes either, as it's garbage collected via reference counting.


Swift isn't garbage collected. ARC is not GC. It's actually a form of manual memory management, with lots of help from the compiler. The way you structure your program does give particular objects a particular lifetime.

https://news.ycombinator.com/item?id=7813598


Reference counting is a form of garbage collection [1] [2], as I've explained numerous times before. It has been considered a form of GC since the original papers in the 1960s and I see no reason to revisit that definition now.

To track the lifetimes of objects, Swift programs perform operations (reference count manipulation) at runtime. That's because the Swift compiler does not know the static lifetimes of objects, unlike Rust. It can optimize away some reference count traffic, but this doesn't change the basic system: semantically, all objects are reference counted.

[1]: https://en.m.wikipedia.org/wiki/Reference_counting

[2]: http://www.memorymanagement.org/glossary/a.html#term-automat...


Patrick already got into the subjectivity aspect here, so I'll leave that subthread there.

  > Rust eschews readability by erring on the side of terseness
We actually used to be more terse, then slowly expanded things until it felt right. At one point in time, "return" was "ret".

  > I can't understand who thought using ' was a great idea.
We had a lot of discussion about this before 1.0, and nobody was able to come up with a proposal that was clearly better.

  > I wish more time was spent on the visual aspect of a language.
We did spend a lot of time on this, and are continuing to. For example, the rustfmt team is just getting going, to nail down the last bit of style guidelines. Rust has had many different syntaxes over the years, and this is what we arrived at. See that other thread on subjectivity; I actually really like the "C with a bit of ML sprinkled in" that we arrived at. The only thing I don't like at all is the turbofish, ::<>, but I understand why it exists, it's almost never required, and it at least has a fun name to take some of the edge off. That said, I've also gotten less and less opinionated about syntax over time; semantics is far more interesting. As long as everything is consistent, I don't care as much about the specific details.


"C with a bit of ML sprinkled in" is IMO quite an understatement. ML is all over the place.

Personally i would have been happier if Rust had become "C with only the borrow checker and nothing else".But the community decided differently, and that's OK too.

I'm a low level guy (embedded and systems stuff) and what i would hate in our codebase is if look at line of code and find x numbers of concepts in it.

Nevertheless i applaud what you and the Rust community achieved, but my impression is that you'll be going the C++ way. Why? I follow the discussions of the Rust community about the future of the language, and there's always this "if we only add this one more feature" (for example see discussion about GC).

P.S.: For everyone saying "just use the features you need", no, this won't work. If they are available, people will (a)use them (see C++).

Edit: Typo.


> it's almost never required

Mate you should try Diesel or Futures. ;)


FWIW that Rust code can drop the last instance of `'a` since it can be inferred:

  fn type_list<'a>(dex: &'a DexFile<'a>,
    off: u32, parse_cls_desc: bool) -> Vec<&bstr> {
In fact, the only reason you need to write 'a anywhere there is to tell the compiler that the & reference and the lifetime parameter to DexFile are the same. If either of those two lifetimes didn't exist (e.g. if DexFile didn't take a lifetime) you could drop them entirely:

  fn type_list(dex: &DexFile,
    off: u32, parse_cls_desc: bool) -> Vec<&bstr> {


The difference with that go code is *DexFile is like Arc<DexFile>, and not like &'a DexFile<'a>. Yes, the go code is more readable, but it comes with additional overhead and data ordering constraints that the Rust code avoids.


I searched for Reactor and found Reason[1] instead. Is that what you meant?

1: https://facebook.github.io/reason/


Ah yes, sorry, it's Reason.


> there's no programmer font that aligns the underscore with the text baseline,

Pretty easy to fire up Fontforge and move the underscore where you want it.


Hi Steve, I've been working on learning Rust from a beginner perspective (know some Python, never written a program with more than 500 loc). However, I do know what pointers are, and get the concept of stack and heap.

My comment is this: It is not immediately apparent that when using &var syntax, Rust is "auto-dereferencing" the variable to get the value. It took a moment to wrap my head around why

  println!("{}", &x); //this can show "5".
Rust's ownership/borrowing mechanism, where variables basically "die" if you don't pass by reference, is confusing until I read the FAQ and those three chapters in the Book a few times.

What I think the Book, or some other reference, needs is a "best practices for newbies" guide. Kind of like how we learn the atom is the smallest possible object in high school, I think it would be ok for a doc like this to tell newbies "You almost never want to pass a variable by value". It would help keep people from fighting the borrow checker.

Let me know if I'm crazy.


Hey hey! So, this specific example has nothing to do with auto dereferencing in the language. It's that Display, implemented for &T, displays the value instead of the reference. This is what you want 99% of the time, so Rust is trying to Do The Right Thing here. If you want to print out the numeric version of the pointer, {:p} will do it. It's a bit tough to document specific implementations at the moment, so it's certainly not as clear as it could be. you're right.

( For reference, this implementation is here: https://github.com/rust-lang/rust/blob/master/src/libcore/fm... note that this is in a macro to do this for all of the traits at once...)

I'm actually re-writing the book right now, and one of the things I'm focusing hard on is the stuff around ownership and borrowing. The current chapters are too abstract, the new ones are focused on String/&str. You can find that here: http://rust-lang.github.io/book/ch04-00-understanding-owners...

  > "You almost never want to pass a variable by value".
So, the issue with advice like this is that it's not always true: you almost never want to pass some things by value, but Copy types, for example, you almost always want to pass by value. It's tricky!

That said, it's true that learning Rust can be tricky; we're trying to focus on improving that in the coming year. So keep it coming! This kind of feedback is very helpful, thank you.


Rust has both “essential verbosity” and “accidental verbosity”:

(0) Lifetimes have to be represented in the language's syntax somehow, and this can't be avoided. Lifetime elision makes this a nonissue, anyway.

(1) Rust's syntax for function declarations is horrendous. In a single place, it does two things that ought to be kept separate: state the type of the function and give names to the function's arguments. This by no means essential, e.g., Haskell does this much better.

(2) The lack of global type inference really hurts when you write functions with syntactically large argument and/or return types. AFAICT, Rust's type system is Haskell's minus HKTs plus higher-ranked lifetimes. Most Rust code doesn't use anything higher-ranked, so Rust could have much more aggressive type inference. The only impediment is the Java-like syntax for method calls: you can't infer nominal object types from how objects are used. This is again purely accidental.


> This is again purely accidental.

WRT 2 and 3, disabling/not doing global type inference was a wilful choice, Rust's designers specifically decided not to do that and to keep type inference local.

Not splitting signatures out of function declarations comes from the same place, having removed GTI there was no point in making type signatures and function declarations independent, and it provides for uniform declaration when there's no function/method body ("abstract" trait methods, extern declarations).


Willful or not, it makes the language more verbose.


I don't think it does, other syntactic decisions (e.g. C++/Java-style generic declarations, reference sigils) might, or at least make the language seem noisier, but with respect to function type declarations you'd get the exact same verbosity (in terms of tokens or characters), just split differently e.g.

    fn class(&mut self, s: &'a bstr) -> u16
versus

    class :: &mut, &'a bstr -> u16
    fn class(self, s)
all you get is repeating the function name.


Think about `where` clauses.


Where clauses don't go away and their verbosity doesn't change.


Trait constraints in Rust are syntactically more annoying than type class constraints are in Haskell. This is in spite of the fact that, semantically, both are exactly the same thing.


      This is in spite of the fact that, semantically, both are exactly the same thing.
Semantics is not practice.

Rust is a compiled language. All Types/TypeClasses have to eventually be resolved down to concrete structures/tuples that can be statically or dynamically dispatched too, and in most cases allocated to the heap/stack.

Haskell is NOT a compiled language. It compiles to a bytecode that is ran by the RTS (Run Time System) a ball of ~50,000SLOC in C/C++. Haskell TypeClasses are MAGIC managed by the run-time. They are not resolved to actual machine code. A compiled Haskell program is just the bytecode+source code+compiler+RTS bundled into an ELF. Yes some Haskell is translated into pure C, but all of it can't be. The pure C Haskell still depends on the run time heavily. Especially for managing type class dynamic dispatch.


> All Types/TypeClasses have to eventually be resolved down to concrete structures/tuples that can be statically or dynamically dispatched too, and in most cases allocated to the heap/stack.

My original remark was strictly about surface syntax issues (verbosity). Surface syntax is handled by the compiler's front-end. I have no idea why this should affect the compiler's backend or the runtime system. But maybe you know something about compilers that I don't.

> Haskell TypeClasses are MAGIC managed by the run-time.

No. Haskell's type classes and constraints are typically implemented as vtables (although Haskellers normally call them “dictionaries”) and extra function arguments, which are resolved at compile time. (With higher-rank types, the compiler can determine that a given vtable has to be passed around between places, though.) The runtime system has no special support for type classes - a type class instance is a record (of methods) just like any other.

Of course, Rust monomorphizes generic functions, and optimizes away the intermediate step of fetching methods from vtables whenever possible. But if you use trait objects (akin to Haskell's existentials), vtables will still be there.


> Haskell is NOT a compiled language

Yes it is

> It compiles to a bytecode that is ran by the RTS

Nope, it's compiled to machine code.

(We're talking about GHC here, of course)


Read the GHC's docs because they disagree with you.

https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts

https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts/Interpr...

https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts/Storage...

https://hackage.haskell.org/package/vacuum

https://research.microsoft.com/en-us/um/people/simonpj/paper...

https://research.microsoft.com/en-us/um/people/simonpj/Paper...

Haskell is a complex hybrid. TypeClasses can't always be resolved to concrete statically sized types, Haskell does RTS Magic for you when this happens. Rust throws a compiler error in this case.


Implementing an abstraction that requires indirection at runtime does not traditionally render the language "not compiled". Any definition of "not compiled" that includes the vast majority of real-world C++ is not a definition in common use.

If you want to point out that "compiled" really ought to be a continuum rather than a binary property, and that by this definition Rust is "more" compiled than Haskell, I'll be right there with you (though the pre-existing word "static" probably fits better), but we'd be creating an idiosyncratic definition not shared by very many people. (Though perhaps it should be. The binary distinction of the ~1970s really isn't that useful anymore, in a world of scripting languages, JITs, and a greater focus on the costs of indirection.)


Haskell type classes are by no means magic and the run-time does absolutely nothing with them.

Type classes are de-sugared in the compiler and turned into an extra function parameter that is a record of functions, aka "dictionary".

This is a bit similar to vtables in C++, but different because firstly, the C++ vtable is already reachable from explicit parameters/implicit "this", and secondly, C++ actually has dynamic dispatch whereas type classes are not dynamic dispatch, at least from a language point of view - they essentially correspond to "overloading" in C++.

Type classes could just as well be implemented by expanding all invocations of the function with different instances and generating monomorphic code for every combination of instances. Compilers don't do that because it would waste too much space.

Also, what exactly is a "compiled language"? The words don't mean much of significance.


> This is a bit similar to vtables in C++, but different because firstly, the C++ vtable is already reachable from explicit parameters/implicit "this"

Rust vtables aren't attached to all objects, but they're always attached to trait objects (whose methods are dynamically dispatched). So it's halfway between C++ and Haskell in this regard.

FWIW, I would call dictionaries “vtables”. C++ and Rust just happen to impose restrictions on how vtables can be used, so that each trait object can be bundled with its own vtable (`exists a. C a => a`), rather than potentially bundling several objects with the same vtable (`exists a. C a => T a`). But this doesn't mean that Haskell uses a fundamentally different implementation technique.

> Type classes could just as well be implemented by expanding all invocations of the function with different instances and generating monomorphic code for every combination of instances.

As long as you don't use higher-ranked types.


> In a systems language, it's important to know if you're passing around things by value or reference.

I'm not sure that distinction should be always in your face though. Systems or no systems, you almost never want to pass a number by reference or pass an array by value.


> Systems or no systems, you almost never want to pass a number by reference

It does occur for generic reference-based API though (e.g. many iterators)

And you'd want to know whether you're passing a string or a vector by reference of value though, especially with affine types.


Why would you ever want to copy a string when passing it into a function?


Note that "pass by value" isn't a copy in a language with affine types like Rust.

I'm not sure passing everything by reference probably would gain much in a systems programming language in the vein of Rust: one would still need some syntactic method to distinguish between shared ("immutable") and non-shared ("mutable") references. Also, Rust-style static reasoning cares about things actually being pointers, to allow returning references safely. Lastly, being able to move things around is important too, it is a major part in Rust creating zero-overhead abstractions.

I'm not sure exactly how MLKit works, but I glanced over https://github.com/melsman/mlkit/blob/master/doc/mlkit.pdf and it seems to me that its regions are somewhat less static and that it doesn't attempt to handle concurrency. I could be wrong on both counts!


I think you could have mut visible at the language level, but otherwise have all parameter passing look and feel like Java, without any pointer noise or implicit copying.


How would lifetimes work? Java & al get away with it because they don't reify lifetimes and the concepts of reference and value are moved at the type level (and they don't use affine types).


From looking at research languages with similar systems (Mezzo, LinearML...) none of them seem to have pointer noise. I might be missing something though.


In Rust, passing a string by value is a move, and it won't copy the backing buffer at most it copies the stack structure (24 bytes) and possibly not even that (depending on the way it's used).

As to why, well because the function asks for it, either because it generically works on values, or because it consumes the string to avoid copying its internal buffer (e.g. https://doc.rust-lang.org/std/string/struct.String.html#meth...), or whatever other reason. Either way that's a completely valid use.


> This means &s to indicate where something is passed by a pointer,

Having had a strong background doing system programming in Wirth based languages, this was something that I never liked in C, and was quite happy that C++ followed the same path as Algol derivatives.


If you're deeply disturbed by all the @s then you formed your opinion more than a year ago.


Sorry mr blub, but you discredited yourself by that response. You might find it great to take another look at rust.


I wish someone would do this in nim and compare the results.

My wishful thinking is that nim would come in at the same number of lines as Python and very close to Rust's performance.


I was thinking the same thing with OCaml. I wrote in OCaml years ago and I still remember how incredibly terse the language is/was. You can accomplish so much with very few lines of code (with the minor exception of the whole reduplicate signatures and way too many artifacts (mli, cmi, ml, etc)). It also compiles much faster than Rust (I'm not sure about Go but since Go doesn't have that sophisticated type system it isn't really fair).

I have to say Rust got fairly hard to read and think about writing code with lifetimes when using closures. I couldn't decide at times whether to just use a trait with some cells or get the closure stuff working. This was in part because of some bugs (about 1.5 years ago) and the fact that their are 4 types function things. It is also probably because I haven't programmed in anything analogous that has lifetimes (I haven't programmed in C++ in more than 1.5 decades).


There is the Python -> OCaml story for 0install:

http://roscidus.com/blog/blog/2014/06/06/python-to-ocaml-ret...

This article in particular tackles polymorphic variants http://roscidus.com/blog/blog/2014/02/13/ocaml-what-you-gain... which solve one of the issues that the author had with Rust (enum subsets)


I recently used that nice article in a discussion on Lobste.rs given its Type System section does a good job showing what good, static typing can catch without developer thinking to test for it. Get in habit of specifying basic stuff right then the compiler pulls its weight a lot. On top of it, key components in ML's get formally verified on occasion due to their easier modeling in theorem provers, etc. The last benefit is the compilation is apparently straight-forward and easier to get right given Esterel managed to certify SCADE code generator in it. That requires source-to-object code equivalence proof which is a bit hard in some of these popular languages. ;)


Sort of tangental but oone thing I have always appreciated about the Python community and Guido is the flexibility and open mindedness in adding features to the language from other languages. I can't remember where I heard/saw it (it was video iirc) but Guido had pointed out there is real value to having static type analysis to a language.

While Python does now have type annotations (pep 484) that are more powerful than Golang (generics) they currently aren't checked. I wouldn't be surprised if future Pythons start readily using these type annotations. At the same time I wouldn't be surprised if Golang never has generic (just my opinion).


that was my first thought too. ocaml would have addressed all the complaints he had with rust:

  * far more concise  
  * familiar exception handling mechanism  
  * classes and inheritance for the few places he needed them  
  * mutability  
  * bytestrings
missing are

  * proper support for enum subset types (i've felt the need for those too)  
  * exhaustiveness checking for uint8  
  * bit length (seems like a very niche thing; i'm sure someone's implemented it in a library somewhere)
rust makes a compelling competitor to d as an upgrade path from c++, but if you're porting code from a high-level, dynamic language like python, ocaml is almost always going to be a better experience.


You can convert an `Option` into a `Result` and then use the `try!` macro, all in a single line: http://stackoverflow.com/a/37890739/46571


But Iterator's next() returns an Option, not a Result.


I don't see why you'd want to manipulate iterators by hand, unless you're implementing custom iterators. But, in any case:

    try!(iter.next().ok_or("end of sequence"))
EDIT: Sorry. I was just being dense.


Check out the code that goes along the "no try! for Option" comment in the OP: it is implementing a custom iterator.


Oh, my bad. I guess `try!` isn't a good enough replacement for properly treating `Option` and `Result` as monads.


> Besides, when nearly every call requires a cast, people will just automatically add them to shut the compiler up, defeating the purpose.

That's why there is little reason to "fix" C integer promotion rules in the first place (apart from unsigned).


Rust syntax is horrible compare to Python / Go, I don't see how you could give an advice to someone that do Python to use Rust, you lose GC and add a great layer of complexity / learning curve. No thanks. Seriously when I see that:

struct IRBlock<'b, 'a: 'b> { pool: &'b mut (ConstantPool<'a> + 'a), // other fields omitted }


GC is not a feature in all cases, and without GC you must manually handle allocation and deallocation in some cases, C or C++ let you free of lifetime annotations at the cost of crashes, Swift force you to use counted pointers and Rust force you in a semantic that forbid errors at the cost of some lifetime allocation that you consider ugly. But it is still shorter than copy pasted Go code to simulate generics, and multiple "err, res" that stains the code.


> C or C++ let you free of lifetime annotations at the cost of crashes

This is so very true. A lot of my time as a professional C and C++ developer is spent trying to figure out exactly how long some previous programmer intended a value to live, whether it can be safely accessed in this context without a lock, etc. The ugliness of <'b, 'a: 'b> is vastly preferable to segfaults or corruption in production and days of debugging.

Anyway, time for me to go back to staring at why this machine isn't reinstalling because wget is hitting a null pointer dereference, but I'm sure wget's code is very pretty.


I basically sigh with relief whenever I'm wrangling with some data in a large c++ codebase and discover that it is refcounted (which tends to be the case for most data where the question of possibly holding it past the lifetime arises).

In rust, my reaction is the opposite -- I'm skeptical of every Rc I see and part of my mind immediately tries to see if it can be gotten rid of.


My point is a lot of people use Python because it's a simple and powerful language, Rust is not a simple language at all. And the performance it brings is not enough to make the decision to switch language in most cases.


I'm not sure what you're arguing. The article didn't advise people to switch from Python to Rust. It simply gave information that people that are considering it. The last paragraph even echoes some of your sentiment.

> And the performance it brings is not enough to make the decision to switch language in most cases.

That's assuming a lot. Nearly 10x the speed is extremely useful for some people. Presumably if you are rewriting your Python code in Rust or Go, you've already identified performance as a big reason to do so, and that gain would be extremely useful.


"Rewriting anything takes a lot of time and causes lots of bugs, going from Python to less expressive language means an increased maintenance burden. But if for some reason, you still want to rewrite it, please don’t pick Go. Rust is better in pretty much every way."

I mean it's pretty much 'never use Go'


Yeah, the article boils down to "If you feel the need to rewrite your python in a statically typed language, and it's between Rust and Go, don't use Go, because the cost of implementing in Rust is roughly equivalent, but the performance gains are much higher."

I think with all the evidence presented, that's an eminently defensible position. Get twice the performance of Go (in this case, obviously it will be very dependent on project) for not much more effort, when you've already made the choice to rewrite for performance.


Rust isn't trying to be "simple", it's trying to solve a specific problem. If you don't need a systems language that has exceptional memory safety, then Rust simply isn't the right language for you. That doesn't make it "horrible" for those who actually need it.


It's not about "switching" languages - it's about making sure the languages you do use solve the problems you care about. Honestly, nobody cares what language you use as an individual other than perhaps your coworkers.


You're not expected to decide to switch to anything. I deal with at least four distinct programming languages every single day I have the good fortune to regain consciousness. Both Python and Rust are often among them. Feel free to learn more than one programming language; no one is expecting you to 'switch.'


Most programs don't need to max out the performance of the CPU. So what? There are a lot that do.


Why is this getting downvoted? Please don't downvote if you disagree with this viewpoint. Rust has its reasons for is current form, which is OK! In general, the whole song and dance around GC is a hard problem.

I agree that the syntax is cryptic. I also think that programs are written by humans for human consumption (for accomplishing a certain task).

If you have a long term plan, you might want to factor in the talent pool, hardware cost, and tech debt. For your case, if that leads to Rust, so be it. Or if it means Python, that is fine too.


Likely because it's presented as a rebuttal to the article, when the article makes no such claim. It's a non-sequitur at best, and possibly a straw-man argument.


I'm curious what the performance would be if it were ported to Haskell or OCaml. It almost certainly would be slower than Rust, but I think some benchmarks would definitely be interesting when comparing those languages to Go.


Really interesting write up. I'd love for you to port to swift and gives us your thoughts! :)


Plenty of ways to .orthat for options. The author should checkout the option type in detail




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: