619 karma · joined August 17, 2015
Also, microbenchmarks are usually not very good at representing the real world characteristics. But then again, please look at the current version of the linked benchmark.[3] Rust now positions itself in the second place, thanks to the recent improvements given to the Rust programs used in the benchmarks. I believe Rust isn't as fast as highly optimized C, but I think it is safe to say that Rust is just as fast as C++, and both Rust and C++ are much faster than GC'ed languages such as Java, Haskell, and Go.
Edit: I would even say that if Rust's performance is comparable to GC'ed languages, there's no point in using Rust in the first place. Rust is paying a lot of complexity to achieve memory safety without relying on garbage collection.
[1] https://news.ycombinator.com/item?id=9531822
[2] https://www.reddit.com/r/cpp/comments/35pn6h/criticizing_the...
[3] http://benchmarksgame.alioth.debian.org/u64q/which-programs-...
Actually, I think GC is inherent to the language, and that's why I listed it as one of the trade-offs that Go made (not the Go implementation made). Almost every language construct depends on GC, e.g. append(), make(), and more. Even the following innocent-looking code:
func f() *int {
a := 1
return &a
}
is GC-dependent, because it would result in a dangling pointer in non-GC languages. But the code is perfectly fine in Go because GC is inherent to the language. It is not possible to make GC optional in Go, so the performance penalty will stay there forever, only alleviated as the implementation matures (or sometimes regressed, as Go 1.5 shows), unless you use only unsafe.Pointer all the time.As for the core features of Go you mentioned, sorry again for my cynical wording, but I think they are a bit dishonest.
They stress on the performance of the program, yet being slower than C/C++ due to the usage of GC and dynamic dispatch all the time. It was even regressed in Go 1.5[1], because the new GC prefers latency to performance. So, they traded performance for something considered more important.
How about the performance of the compiler? Well, the speed of the Go 1.5 compiler is ~10% worse than the previous one, because they rewrote the compiler in Go (it was previously in C, for those who didn't know yet). Why did they do so? Because it has advantages: reduces the barrier to contribute to the compiler, improves the code quality, etc.. Yet they traded performance for something they consider to be important.
And the performance of the developer... While Go is not very terrible at developer experiences, it isn't very good either. Embracing simplicity led to lack of expressiveness, a typical example being generics. While this isn't entirely a bad thing, it is, again, a trade-off.
So they make trade-offs all the time. Then why are generics not considered to be good enough to make a trade-off? Because they think so. It is their opinion, sure, but claiming generics are impossible because performance isn't very convincing to me.
[1] http://yokohummer7.github.io/blog/2015/09/01/go-1.5-regressi...
Windows gets over this problem by letting the login shell (explorer.exe) reload the environment variables in place. So, if we are to do the same thing in Linux, we might have to patch bash, zsh, and GNOME etc. to understand the environment variable change events and reload itself appropriately. I'm not sure where to start, but I believe this is the one thing that Windows did right, so I want to see it happen.
Also note that free single domain certificates have been available from StartSSL for a very long time, but this didn't destroy the certificate industry.
Hearing this leaves a bad taste in my mouth, because one of the (mis)features I've found in Go is its implicit default value in struct initialization. In Go, you don't get a compile error when you miss some fields while initializing a struct. e.g.
type T struct {
a int
b string
c float64
}
t := T{c: 1.5}
will happily set `t.a` to 0 and `t.b` to an empty string. While this is useful for maintaining backward compatibility (adding more fields to a struct doesn't break upstream code), it also hinders discovering genuine mistakes at compile time. So typical Go programs end up using 0 or an empty string as a sentinel value. This is probably what made me feel Go's dynamic nature the most. It really is pretty close to a dynamic language.The only exception I think is the bool coercion. Even though I can predict the behavior, I find it fairly disturbing. I believe that should have been removed when transitioning to Python 3. It is very unfortunate, considering Python is generally reluctant to implicit type conversion.
I think the more important reason is that Python doesn't really have an emphasis on the boolean type in general. The `if` statement works for every type, in contrast to other languages allowing only the boolean type in `if`. It is very common to use `if string_or_number_or_anything: blah blah` in Python. While I won't make a stance whether this is a good thing or not, it is still awkward that Python allows implicit conversion between `bool` and `int`.
FWIW, Russ Cox's GitHub icon[1] is similarly scary to me.
This is probably the reason why our opinions vary. I believe rewriting existing libraries to be safe would be a tremendous amount of work. This estimation comes from my experiences with Rust. Safety guarantee hugely affects the interfaces of the libraries. You cannot "just" gradually increase safety of your library. It is basically a redesign. At least with what Rust currently provides this was my experience. And the method suggested by Herb Sutter seems to be very similar to Rust's. So I expect the same churn to happen in the C++ ecosystem, and it would effectively split the ecosystem into two: safe ones and unsafe ones.
Let me rephrase myself again: if adding safety to C++ in a gradual manner was that easy, Rust would have not been invented in the first place. The Mozilla guys could just use C++. There's a reason why Rust had to exist, despite all the "reinventing the wheel".
- If you enforce memory safety to all of your code, the number of the libraries that you can use will be severely limited. Imagine if you can only use C++14 libraries. How many C++14 libraries are out there? And I'm not sure if there will be many libraries written in safe C++, at least for the next few years. The adoption rate for newer C++ standards (C++11, C++14) was generally not satisfactory.
- If you only enforce memory safety to your own code, then you could use the existing ecosystem. But in that case, your program will still segfault because of the libraries you're using. Admittedly this is also a concern of Rust when you use C libraries in Rust. The more safe code you use, the safer your program is. The problem is, if you choose this route, there's not much ecosystem-ish advantage in the C++'s side. You will also have to "reinvent the wheel" as you would have to do in Rust.
This problem all boils to this: adding guaranteed memory safety to the unsafe language is a breaking change. And the problems occurred by breaking changes are very hard to solve.
- Haskell: While pervasive laziness does have its merits, I believe it is not the best choice in general, especially for performance-wise. Having to use streaming libraries to do even a simple I/O is just ridiculous. Its syntax was also a big obstacle to me, but thankfully I overcame it. But I'm not sure I can also persuade my colleagues to do the same thing.
- Scala: If I have a JVM requirement, Scala would be a really good fit. (Un)fortunately I don't have such a limit, so what Scala provides are less appealing to me. It also seemed way too gigantic to me at a glance, probably to be interoperable with Java. But I've not tried it deeply.
- F#: I've only heard good things about that, and it was quite a great experience learning through its great IDE. The problem is that even though MS's recent moves I'm still not completely sure if I could retrofit myself to the .NET ecosystem. But the future is bright.
- OCaml: OCaml always felt a bit "obsolete" to me. AFAIK it even didn't have multi-core support until very recently (no parallelism, concurrency only), and they seem to have no resources for a decent Windows support. It has also a funny syntax. I definitely agree Rust's syntax is ugly at best, but OCaml's is... I just can't describe it. Hopefully I can get used to it someday, like I did to Haskell.
And I met Rust and instantly fell in love, even though it requires me to do manual memory management. While manual memory management makes a certain data structure harder to implement, what I realized is that for my daily job, especially for "boring" works (e.g. CRUD apps), it is OK to use manual memory management. If there's a need, I can always borrow libraries from others to accomplish my job. And it is fast by default, less need to optimize the code by myself, as seen in Haskell.
That said, what I have as a "best" language in my mind is, probably a strict version of Haskell. Yeah, that would be Idris, like the author suggests. I really hope that Idris will take off in the near future.
The problem is, to completely solve space leaks, Haskell should abandon pervasive laziness. But laziness is sold as one of the best things about Haskell, and most Haskellers do love laziness because it improves composability.
I myself hate pervasive laziness, and I'm all for removing it from the language. Unfortunately this argument will fail to convince most Haskellers because they're already sold on the bright sides of pervasive laziness.
Ok, this is all good, but from here there's one really annoying aspect of the Haskell community. They try to twist the reality to convince themselves. The typical reaction when these kinds of arguments are brought on the Haskell community is "space leaks are actually rare in the real world applications". Yes, this is the saying from the very community that emphasizes on program correctness. Sure, buffer overflow and memory leaks also rarely happen in other languages? Isn't "exact resource usage" also a part of correctness? At least in a broader sense?
Even SPJ (one of the designers of the language) admits laziness should retire now (it was a good experiment to develop purely functional ways, but now the disadvantages outweigh the advantages), but most Haskellers will never agree with this because laziness does also have advantages, and they love it. I actually gave up waiting for the Haskell community to abandon pervasive laziness, so just moved to other languages.
Sometimes I get a sense that many Haskellers treat hardware as merely implementation details.
The funny thing is that this feature also explains what Go is. Go relies heavily on the various tooling just to overcome the deficiencies of the language. It's just that they didn't choose IDE as a primary target, but instead various ad hoc tools.
For example, when I find the unused imports being a hard error inconvenient, a typical suggestion from Gophers is "just use an editor plugin that automatically handles that". This is exactly the same attitude that can be found in the IDE lovers.
And that's why I don't like Go anymore. In the name of the simplicity of the language, they concentrate on building more and more tools, rather than fixing the language. I'm pretty sure if Go acquires generics someday, it will have the form of code generation, which is proven to be awkward. (I heard "go generate" already does a part of that?)
Unfortunately I haven't seen any comprehensive article that discusses affine types in terms of Rust. So I can't recommend a link to you, instead I'll try to explain them in my own words.
Affine types are those whose values can only be used at most once. They are implemented in Rust as ownership and borrowing. In my example above, `send_header` borrows the `conn` variable, whereas `send_body` takes the ownership of the `conn` variable. As you lost the ownership of `conn`, you cannot use it anymore, thus resulting in a compile error.
This is similar to C++'s move semantics, where you are not permitted to move the same value more than once. It is just that in C++ "move only once" is a convention, whereas in Rust it is enforced by the type system.
In Rust, if you pass an argument by value, you're automatically using affine types. You cannot use the passed variable anymore. To "borrow" instead, you need to opt-out using references. So, the hypothetical API I mentioned above would be written as:
fn send_header(&self, text: &str) { ... }
fn send_body(self, text: &str) { ... }
Note the difference of `&`.Finally, all of these aren't just hypothetical. The exactly same design is used in Hyper[1], which is an HTTP library, and in nickel[2], which is a web framework in Rust. If you look at the API doc you will see there is no '&' in the self parameter. That means it's passed by value, using affine types, and the ownership is transferred, so you cannot use the original variable anymore.
I hope this helps.
[1] http://hyper.rs/hyper/hyper/client/struct.RequestBuilder.htm...
[2] http://docs.nickel.rs/nickel/struct.Response.html#method.sen...