Hacker News new | past | comments | ask | show | jobs | submit login
Rust via its Core Values (designisrefactoring.com)
135 points by pcwalton on April 3, 2016 | hide | past | favorite | 119 comments



This should be titled "A Ruby programmer looks at Rust". There are now a very large number of programmers who have zero experience with anything below the level of a scripting language, and have never had to think about memory allocation. To them, Rust is rather intimidating, and this writer is trying to make it simpler.

Unfortunately, it looks like he doesn't really understand borrowing. The key idea of borrowing is that a borrowed reference can't outlive the thing it came from. It's a lifetime thing, not an immutability thing.

Go is a hard-compiled language compatible with the mindset of scripting language programmers. That's just what Google needs. They have a lot of server side code to write, and it has to go reasonably fast or they have to build additional acres of data centers. With Go, they can put programmers from the scripting language community on the job.


Your analysis is good except that last part. Google hires tons of geniuses who could handle Rust easily with a bit of training. The extra efficiency would reduce datacenter requirements even more. Most of their tools are also internal where scripting community has minimal contribution.

So, I don't see a clear benefit for them to choose Go over Rust except saving face. They'd be better off investing in both for various reasons with critical stuff like F1 RDBMS in Rust and rapid dev in Go.


"The key point here is our programmers are Googlers, they're not researchers. They're typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They're not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt."

-- Rob Pike (http://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Fro...)


Rob Pike has his ambitions. Great. Meanwhile, almost no one at Google uses Go. They use C++ and Java almost exclusively. Not Python either, outside of Youtube. (There's a reason GvR left.... all of the stuff he was proud of writing in Python got re-written, e.g. Mondrian.)

Pike is right that a significant fraction of Googlers are young and fresh out of school, and aren't 'researchers' (like maybe 33%?). They're also crazy smart, and can learn whatever language you need them to. Note that Google makes new grads learn a new storage model (BigTable etc), learn a bunch of custom infrastructure (everything except compilers and VCS is custom-built), learn all sorts of crazy shit. What Google does with these facts about their developers is that it uses C++ for huge-scale things (underlying infrastructure like BigTable, or massive-scale products like Search) and Java for the less-incredibly-demanding. Not Go. Unless you're Rob Pike and someone asked you if you could take care of something that needed a rewrite anyway.

It only makes sense to talk about Go as a Google language if you are under the mistaken impression that there are about 40 engineers at Google.

Look at https://golang.org/doc/faq#Is_Google_using_go_internally That's basically an admission of defeat. "We eat our own dogfood, and also a few tiny projects use it (one of which is tiny but super important)." Note that "scaling MySQL" is not exactly a long-term priority in a company that built three or four alternatives to MySQL, all superior.

It's possible that, if Google were starting from scratch, that ideas like "write the fast stuff in Rust and the less-urgent stuff in Go, which is still fairly fast" would be good choices. Sounds like a good idea to me. But the value proposition of Go over Java is, I suspect, not big enough to bother turning the enormous ship.

I'd love to hear any current Googlers chime in about a Google product written in Go that actually matters and took more than a hundred engineer-hours to write.


The linked section of the FAQ was written a few years ago. I'm not a Googler, but I can tell you some things visible from the outside.

>Note that "scaling MySQL" is not exactly a long-term priority in a company that built three or four alternatives to MySQL, all superior.

It might be more valuable than you think since GCE sells a hosted MySQL service. I'm speculating here; they could be using something else that's MySQL-compatible, but I doubt it.

>I'd love to hear any current Googlers chime in about a Google product written in Go that actually matters and took more than a hundred engineer-hours to write.

Kubernetes is a non-trivial Go project largely done by Google that's important to the future of GCE.


How is Kubernetes important to the future of GCE? I don't believe thats the scheduler they are using for compute. I know you can run Kube on top of GCE but that's obviously not the same thing. I was also told by someone at Google Cloud that when you launch a compute instance on GCE its actually running inside a VM as well. So this container inside a VM leads to me to think this is true. The VM might be for security, I'm not sure.


Thank you, those both seem like convincing evidence that, at minimum, it's not quite as bleak as I had believed.


Note, however, that kubernetes is not used within Google (rather, they use a C++ alternative).

Part of why kubernetes uses Go is that it's in a docker-centric container ecosystem. Docker picked Go for its own reasons, not because of Google, so in a way, Kubernetes is picking Go because of the outside community, not because of Google or the language itself.


GCE operates GKE which is a hosted Kubernetes as a service. I'm not saying that Google is using Kubernetes instead of Borg, but that doesn't mean they don't use Kubernetes at all.


TheParent's explanation makes more sense. Note that the key use case they mentioned for that was an offering for the Docker ecosystem:

http://www.infoq.com/news/2014/11/google-cloud-container-eng...

So, people are using Kubernetes in the Docker community. Google offers a GCE service for that. This seems incidental to Go with community and demand really driving its use. That they avoid it internally where possible is still a strong point given the large ecosystem developing around Go. Assuming it's true that they avoid it for C++ and Java of course.


Oh I was just commenting similar to this above. Is the C++ alternative Borg? I believe this spins up a VM like KVM and the runs the container inside the VM.


Does a mysql compatible product include the bugs? :)


Not that it's necessarily better if the projects you are making take over a hundred hours to write, because it does suggest an attraction to monolithic designs.


That vontradicts the reported justifications for their selection and interviewing processes. He's telling us they can handle all that but not learn Rust like many average programmers are doing successfully? Does. Not. Compute.


EDIT: quote at 20:40. Not a response to the question.

Do you have a timestamp for this quote? It would be interesting to hear the context--what was said before and after. Was this a response to a question?


That's a good point.


Yeah he said that, but presented no evidence to support his claim.


> Google hires tons of geniuses who could handle Rust easily with a bit of training.

With the quality of tooling we get on Android, I always wonder where those geniuses work.


Lmao. Smart != wise. I'll take above-average, experienced developers w/ track record in good deliver vs QA tradeoffs over new geniuses any day. Google probably does the opposite. I imagine that's what brings such results.


To them, Rust is rather intimidating

I'd argue that Rust's concepts of ownership, borrowing and lifetimes are rather intimidating even to people who have had to think about memory allocation. You can get part of the way there with analogies about stack-allocated vs. heap-allocated in other languages, but ultimately it's just a concept that, say, the average C programmer is going to find alien and difficult at first (largely because it's designed to prevent common types of errors committed by the average C programmer).


> This should be titled "A Ruby programmer looks at Rust". There are now a very large number of programmers who have zero experience (...) To them, Rust is rather intimidating, and this writer is trying to make it simpler.

Funny. I find Rust a more accessible language than Ruby. Rust is certainly not an easy language to learn, but the basic concepts (ownership, borrowing, lifetime) are based on everyday life, and the analogy is precise enough that I don't need to worry about a mismatch between my intuition and the language. For instance, if I let my friend borrow my car, I can't drive it myself until he returns it to me (freezing). And, if a married couple owns a car (Arc), they must take turns to drive it (Mutex, RwLock).

On the other hand Ruby's design principles seem to have been:

(0) The syntax has to look like English. This is non-negotiable.

(1) Each feature must be easy to explain (FSVO “explain”) in isolation.

(2) Interactions between arbitrarily many features are allowed to be arbitrarily nasty.


Ruby doesn't look like English. Hypercard:

    put word 2 of the clickLine into lino
and COBOL-60:

    MULTIPLY A BY B GIVING C ON SIZE ERROR STOP RUN.
looked like English.


Point taken. I was thinking about how Ruby DSLs are often designed to resemble English text as much as possible.


> Unfortunately, it looks like he doesn't really understand borrowing. The key idea of borrowing is that a borrowed reference can't outlive the thing it came from. It's a lifetime thing, not an immutability thing.

As a rust newbie, this touches on something I've been trying to figure out how to think about. Should I think of `&` in terms of ownership exclusively, or is it also useful to think about passing a pointer vs copying the data?

That is, suppose I have something that, semantically, I want to move into a function, and which the function later returns. I think I could also pass a `& mut` instead. I'm trying to decide how to reason about that decision. Is the compiler smart enough to turn moving/returning into passing a pointer behind the scenes? If it's a large struct do I need to think about that "optimization" of instead passing just a `&` instead of moving the whole thing?


  > Is the compiler smart enough to turn moving/returning 
  > into passing a pointer behind the scenes?
Rust guarantees RVO, yes, and this is even baked into its calling convention.


> That is, suppose I have something that, semantically, I want to move into a function, and which the function later returns. I think I could also pass a `& mut` instead.

That is exactly what `&mut` is for. An &mut gives you an "ownership lease" on an object; you own it as long as you have the &mut.


> This should be titled "A Ruby programmer looks at Rust". There are now a very large number of programmers who have zero experience with anything below the level of a scripting language, and have never had to think about memory allocation. To them, Rust is rather intimidating, and this writer is trying to make it simpler.

C++/C doesn't have ownership concept, it may be similar to references but when starting on rust I had to change mindset and constantly fought compiler.

Article itself is interesting because it explains why there is certain syntax, and not a tutorial how you should code in rust.


C and C++ have very strong concepts of ownership. Anything you allocate must be deleted exactly once. Any use of an allocated object must be during that object's lifetime, before deletion. Access beyond the end of an allocated object is prohibited. Violate these rules and your program will crash, garble data, or be exploited.

But C and C++ don't provide any language level help to programmers who must make their code obey those rules. Rust does. That's the great advance in Rust. The ownership model is explicit and checked.


You have a point, however, what rust enforces is more strict than what you need to adhere to in order to write a correct C program.

The proof of this is that several core concepts that are considered "safe" have "unsafe" portions that make let work. Thus, there are safe things that rust doesn't consider safe, or that rust cannot infer is safe.


  > The proof of this is that several core concepts that 
  > are considered "safe" have "unsafe" portions
I don't understand this argument. Rust could have built those features (e.g. `Rc`) directly into the compiler itself and it wouldn't have made the language itself any more safe, and it wouldn't have made the safe bits of the language any more or less powerful.

In fact, `Rc` was once a first-class part of the language itself, and was indicated by the `@` sigil. The fact that it now lives in the standard library rather than the compiler is because library code is easier to audit for correctness than the compiler internals; to prove that the language is powerful enough to permit user-defined smart pointers and memory management primitives; and to permit alternative implementations of basic language features to be swapped in and out via custom standard libraries (which is way easier to do than forking the compiler).


> Thus, there are safe things that rust doesn't consider safe, or that rust cannot infer is safe.

That's also true for any programming language that claims to achieve safety in any sense. At some point you have to have a trusted computing base, whether it's your hardware or the standard library.

In other words, the presence of "unsafe" in the implementations of some things doesn't make Rust unsafe--if Rust is unsafe then so is every other safe language.


I think that swsieber's point was not that Rust is unsafe, but rather that Rust is too paranoid. There exist safe things that Rust will not let you do (without turning off the safety catches)!

Of course, the answer to that is still pretty much exactly what you said: it's true of every programming language that claims to achieve safety in any sense. If Rust is too restrictive then so is every other restricting language.

swsieber, according to says some guy named Gödel, every type system that is sound (and decidable) is not complete. Since decidability is kind of not optional, and most people are not okay with your type system sometimes telling you that something is okay when it's not okay, well, you're gonna have excessive constraints in your language.


Decidability actually is optional; some folks experimenting with dependent type systems are bullish on giving up decidability (allowing type checking to fail to terminate in some cases). Typechecking is already 2^(2^PROGSIZE) for, say, ML. That could easily - in theory - lead to impracticably long compilation times. Yet in practice, it doesn't. So why not go whole hog?


I'm pretty sure the answer actually is that certain things are provably safe, at least in a way that can easily be accomplished by the compiler, and some aren't (at least simply by a compiler at our current understanding). We may be able to reason about a situation and prove to our understanding that something is safe, but that doesn't always mean we can encode those rules in a deterministic and terminating way.

I imagine rust would happily extend what it considers safe if it can be determined in an effective way and if it requires additional decoration, doesn't conflict with current syntax.


> The proof of this is that several core concepts that are considered "safe" have "unsafe" portions that make let work

I think this is actually a common pattern in many different domains. For example, the OS kernel's virtual memory subsystem does many things with "unsafe" primitives -- it has direct access to page tables and can map anything anywhere -- yet it provides a "safe" abstraction of isolated process address spaces.

The way I think about it is that you have to define basic building blocks somewhere. It's not reasonable to build a static analysis that understands the N different varieties of smart pointers, and refcounting, and dynamically-checked mutability (RefCell), and custom allocators (TypedArena), and all that. It's much more elegant to separate concerns, have only raw pointers and lifetimes/borrowing built-in, and put those pieces together in "blessed" ways with all the unsafe code in one place in the library. If you try to build that understanding into the compiler instead, you're just moving the same unsafe-has-to-be-correct algorithm down one level, and unnecessarily complicating things.


> You have a point, however, what rust enforces is more strict than what you need to adhere to in order to write a correct C program.

And C enforces stricter type checking rules than what you need to adhere to in order to write a correct Python program. This is not a bad thing; having an automated assistant that can catch errors as you make them is highly useful. Rust simply pushes this to the domain of memory management and once its rules become more engrained in our development habits, I'm sure it'll be about as painless to deal with than it is to deal with type checking.


Too late to edit, so here it is in a separate post:

My point is that rust is a little more strict than C (even correct C). So even if you are a good C programmer who writes without memory leaks, you'll still probably have to battle the compiler for a bit.

Not hating on rust or its abilities - I think it's great.


Is Rusts concept of ownership any different from C++ unique_ptrs?


As an example, here are two roughly equivalent programs. First Rust:

    let x = Box::new(5);
    let y = x;
    println!("{}", x);
Now C++:

    unique_ptr<int> x(new int(5));

    auto y = std::move(x);

    cout << *x << endl;
(I'm being a bit sloppy with namespaces, and both need a main(), but you get the idea)

Here, the C++ will compile and (probably) segfault, as x is nullptr after the move. The Rust will fail at compile time, with "use of moved value."

Basically, a (possibly over-)generalization of the situation is "Rust is move by default and captures errors at compile time, C++ is not move by default and cannot statically prevent as many errors as Rust can."


It's not identical (Rust statically enforces a lot more than C++ does, move semantics are opt-out rather than opt-in, there's no such thing as move constructors in Rust, etc.), but if you understand unique_ptr then you'll understand Rust's concept of ownership.


I like this a lot, it's always tempting to think that we can create a perfect language that would be the best in all situations but we can't.

Rust is great for projects where its values are valued:

- Speed

- Memory Safety

- Concurrency

Go is great for projects where its values are valued:

- productivity (from being able to immediately understand a new code base due to the small and simple language to the toolchain it provides). - concurrency

Likewise, Ruby and Python are great choices for some projects.


> it's always tempting to think that we can create a perfect language that would be the best in all situations but we can't

Why can't we design a language that would be the best in all situations? I see no reason necessarily to believe that's true, though I recognize that it has not yet been done.

It may be impossible to create a car that is also an excellent submarine and a great airplane, but software is not subject to the same kind of physical limitations. I see no reason in principle why a perfect language could not be malleable enough to accommodate all situations optimally. I realize that my writing here is not a convincing assertion that such a language can exist. I recognize that. I'm just saying that there doesn't seem to be strong evidence that it can't exist or that we should give up trying to design it.

A person who wishes to argue that the perfect language can't exist should perhaps give an example of two different problems that cannot be solved well by any single known language, and that can be solved much better by two programs in two different languages. I would like to dissect that example and see if we can indeed find such examples of sets of problems that cannot be solved well by one language. If we cannot find such problem sets, then that may suggest that we can indeed "unify" programming under one perfect language, by tweaking its syntax and semantics appropriately.


The best possible a language can be for a given situation is that the empty program solves the problem you're trying to solve.

Obviously, in no language can the empty program solve all problems. QED, there is no language that is optimal in all situations.

If you want a more useful answer, you should define what you mean by "best in all situations".


My go-to example is shell scripting. In a shell language, you really, really don't want to be forced to quote strings, and you typically also don't want to have to explicitly parenthesize command invocations. Take a typical shell command:

  rm -rf foobar
You may not think about it much, but these are all just strings. Go ahead and type this into your shell, it will have the same effect:

  "rm" "-rf" "foobar"
Now imagine how utterly annoying it would be to use your shell if you were forced to quote every string you ever typed. Even worse, here's how the above would look in Python-esque syntax:

  call(["rm", "-rf", "foobar"])
Sysadmins would riot in the streets.

Most languages that are explicitly designed for scripting offer this feature: TCL and Perl both allow unquoted strings (Perl with `use strict` places some limits on them), and I believe Ruby has some way of enabling this as well with `method_missing`.

But it's important to note that this behavior is almost never what you want for programming in most other domains, which is why most other languages (particularly non-dynamic ones) don't even offer anything resembling this feature.

And of course, you can have a single language with both! You could have it controlled by a flag or something. Super great. But this is just the tip of the iceberg. When you import a namespace in your language, are all its identifiers automatically brought into your local scope, or must you import them one-by-one? For fast-and-loose tasks you want the former, for long-term maintainability you want the latter. But we could have a switch for this too. Still great. Now, when you `x = new Blah()`, is that Blah on the stack or the heap? This has enormous implications for both semantics and performance, and if you truly want an all-purpose language then heuristics like escape analysis won't save you, you need to be able to control this. On and on we go, until your language has a million little switches that may interact with each other in surprising ways, because different programming tasks actually do have different needs, and although your language may ultimately be capable of fulfilling all these tasks, in practice it will be considered a dozen different languages trying to coexist in the same compiler (with no consensus on where to draw the lines that demarcate each).

Meanwhile, people who want to Get Things Done will opt not to wrestle with your Swiss-army language and will instead favor more limited languages that fulfill the needs of their specific tasks, because the vast majority of people are not writing client-side code in the morning and device drivers in the afternoon.

Now, there does exist a scenario where One Language To Rule Them All can dominate: when the market for programming languages is very small. If the market is small, then demand for domain-specific languages is less. Or rather, the value of differentiation is less, because your differentiated language will have a small share of a small market, and will be unable to thrive. But in a large market, even a small share is enough to subsist on. In reality Go and Rust likely have 1/1000th the number of programmers as Java and C++, but even 1/1000th of tens of millions is more than enough to support large and thriving communities.

So ultimately it's not that it's impossible to make a language that's great for all tasks (though it would still be difficult), it's that there's not enough demand for one, and if you believe that the cohort of programmers is growing then demand for one will only continue to decrease.


my guess is this is because of hardware limitations and the absence of the mythical sufficient smart compiler.

in the one language you write to appease the hardware, which makes it fast. the other language appeases the programmer, which makes it productive and safe. the two goals are irreconcilable as long as the processor and the (the mind of the) programmer work in a different way.

the simplest example i can think of are array boundary checks. as long as memory is finite you have to manage allocated ram. is it possible to know beforehand if an out of bounds access will happen? no; not without certain restrictions.

i'm pretty sure at the core there's some undecidability/halting state problem. or the CAP theorem; consistency, availability, partition tolerance. it's impossible to provide all three: so you might have two languages that solve two different parts of CAP, but leave the third one to the programmer. depending on the problem at hand one language might fit you better than the other.


I think what you will end up with then is Java? It works everywhere and has features for everything and has support for almost all programming language concepts/paradigms or comes very close. We probably just need to fine-tune/minimize Java to arrive at the final perfect language :-).


Tacking on another example to kibwen, while a language can be multiparadigm, where one paradigm is in conflict with another, one of them must win. Further, the language generally must choose some default values (as in philosophical values, not variable values) which will become ingrained into the core library and shape the entire rest of the ecosystem.

If you agree in the utility of multiple paradigms, then there can not be one language that does them all. If you're going to be a logic language like prolog you're going to have to privilege syntax and semantics to make that work. If you're going to be a query language like SQL, you're going to have to privilege syntax and semantics to make that work.

If you agree with the utility of multiple different language values, you can't have one language that does them all equally well. If your language permits mutability, it will work its way throughout all the standard language code and all the library code, and once you have that you basically can't write immutable programs anymore. If your language is based on immutability, all the library code will be based on that and it will be difficult to drop that last "log n" factor that immutable code often imposes on your runtime, plus some of the other characteristics it has that can cause trouble with things like cache coherency. In both cases you can layer something else on top of the core language that may recover the functionality, but the second layer addition will always come with a lot of caveats, sharp edges, and poor interaction with the rest of the standard library. There are many of these dimensions where a language must pick one place on the spectrum, and even if you pick "in the middle" that never ends up meaning "the best of both worlds with none of the drawbacks!"... it's just a point in the middle.

"A person who wishes to argue that the perfect language can't exist should perhaps give an example of two different problems that cannot be solved well by any single known language"

I want high reliability code that I would literally trust my life to, that can be verified both by the compiler and by any other arbitrary external tool that may assert proofs of things that may be desirable, such as "it never crashes as long as the hardware operates correctly" and "there is never a null pointer exception".

I had a cool idea last night, and I want a website up that implements it by next week.

You will never be able to bridge the gap between someone who wants to slap something together quickly and someone who wants to write provably correct code. I don't think we're at the Pareto optimality frontier and that there is still some improvements to be made in general, but even after those improvements, the two ends of that spectrum will forever be separated by a huge gulf, and no one language could possibly straddle it. You can't practically script in Idris or Coq and you can't practically write provably-correct code in a dynamic scripting language. In either case, by the time you made the changes that might permit it, you would no longer have the original language, i.e., TypeScript may take many steps towards letting you write reliable Javascript, but it's no longer the same language anymore, it's a new one with a JS compile target.


"I want high reliability code that I would literally trust my life to, that can be verified both by the compiler and by any other arbitrary external tool that may assert proofs of things that may be desirable, such as "it never crashes as long as the hardware operates correctly" and "there is never a null pointer exception".

I had a cool idea last night, and I want a website up that implements it by next week."

We're closer to that than you think. The pieces of it are just scattered in quite a few CompSci subfields. Some are done, some getting closer, and a ton of integration work will be necessary to pull it all together. There will still be a gap between the two. Yet, it's nowhere near as big as people think with the right tooling available.

Example of the first such tool from the same woman that founded robust, software engineering:

http://htius.com/Product/Product.htm

A NASA evaluation indicated difficulty with notation, performance hit, and something else. Thing is, modern work in each area that report griped about got to point where gripes should be eliminated. There's also more automation available in some areas with better results. The good DSL's and tools like Cyc showed the body of knowledge necessary for semi-automating the dev process can be developed. Hell, so did StackOverflow's cut-and-paste-driven development. So, the dream is a dream but a similar reality isn't far off if the labor is put in. :)


Like I said, I don't think we're at the optimal point yet.

However, when it really comes down to it, writing reliable software and bashing some libraries and bits and pieces together require fundamentally different mentalities. To see that clearly, note how you can learn in just a few weeks to bash libraries together to do some nifty things, but to learn how to write high-quality provable code is always going to be a multi-year enterprise, even for very, very smart people. And you're never going to be able to get rid of that "bashing together" use case... you can offer Bob the manager a spreadsheet that will only produce provably correct spreadsheets for some suitable definition of "provably correct", but Bob ain't gonna use it. It's way more work than he's interested in doing.

A programming language can't force the user to care. If the programming language requires a level of caring in excess of what the user wants, they will use another programming language.


That's true. The mentality is more important. There will consistently be people who barely care.


Productivity in Go is an illusion but I see the appeal of it for big enterprise teams composed of disposable programmers.


  I see the appeal of it for big enterprise teams composed of   
  disposable programmers
Can you explain? Why are go programmers "disposable?"

Is it because the code is understandable (especially following our style guide / enforcing gofmt, godoc, etc)? I do find our go code to be that way, but I don't think that's a bad thing.

  Productivity in Go is an illusion
I feel productive in Go. Can you explain this statement? I like dealing with a simpler toolchain as the op mentioned.


I'm not the parent, but my impression is that Go is productive more in the sense of quantity than quality. It is easy to pick up, understand and use. But it is also very error prone, because its type system is poorly suited for proper error handling, nulls and generic code. It's certainly better than C and most dynamic languages, but that's not setting the bar very high.

I really see no reason to pick up Go these days unless you're a fan of 80s retro hits. If you want most of the goodness of Rust combined with the easiness of Go, look to Kotlin and Swift.


  It's certainly better than C and most dynamic languages, 
  but that's not setting the bar very high.
I'm not sure I agree with that sentiment; C and dynamic languages are used in important applications and being better than them for the same tasks does seem to be a design goal of go. I personally feel that it accomplishes this in a number of areas.

  It is easy to pick up, understand, and use.
This is true and I think it's a huge benefit.

  But it is also very error prone, because its type system 
  is poorly suited for proper error handling, nulls and 
  generic code
That's your opinion. It's less safe than rust, more safe than python. I think it has a rather large niche. I personally like interfaces and that idiomatic golang testing uses language primitives.

It's weird to me that people compare Go and Rust, they're languages with very different motivations.


> C and dynamic languages are used in important applications and being better than them for the same tasks does seem to be a design goal of go

C is essentially popular because it's popular. And it therefore has a huge ecosystem, can run on pretty much anything and has tons of code already written. That's a huge benefit, and one that Go does not have because it hasn't existed as the dominant language for 45 years yet.

Popular dynamic languages have the benefit of being toys with the bare minimum of features needed to scale beyond toy programs. You can build a house with legos too. It won't be a good house, and it won't be very cost-effective, but it's certainly possible.

> That's your opinion.

No, it's not just my opinion. A well-designed type system eliminates the possibility of you making certain types of very common errors. It will refuse to allow you, and it will sometimes even help you understand why, and what you should have done instead.

The complexity of Rust comes mostly from how the type system has been designed to handle manual memory management. If you take that part out and replace it with garbage collection, you'll get a language that is both safe and easy to learn and understand. Not quite as safe as Rust, and not quite as simple as Go, but very close in both regards. Languages like this do exist. See Kotlin and Swift, for example.

> It's weird to me that people compare Go and Rust, they're languages with very different motivations.

In this we agree. And I'm not arguing for Rust as a substitute for Go. But I am arguing for ignoring a language that ignores much of what the rest of us have learnt about type systems and programming languages during the past half a century (in real years, not tech years).


Swift isn't garbage collected. (Allocation and deallocation code is handled by the compiler).


Depending on who you talk to, reference counting is garbage collection. Industry tends to mean "tracing GC" when they say GC, academia tends to mean "all runtime automatic memory management schemes".


I might be totally off-base, since unlike you I have no background in compilers, language runtimes etc, but I was under the impression that Swift's ARC is compile-time, which would make it not GC'ed by either of your definitions, whereas runtime ARC, like PHP's implementation, would fit the second.


It's all good! This is a very subtle difference.

So, imagine an interface to a reference counted thing. You have two methods:

  rc.add()
  rc.subtract()
Add bumps the count, subtract drops the count down. When the count hits zero, the thing is deallocated.

What Swift's _automatic_ reference counting is insert the calls to add/subtract _for you_, so that you don't need to do it. But fundamentally, they're still there. So the decision of "when does this get deallocated" is still a runtime thing, even though those calls are compiler inserted.

Does that make sense? Here's another reference: http://stackoverflow.com/questions/6385212/how-does-the-new-...


Thanks for the info, and for the very clear explanation!

Obviously, even in C the memory management is a high-level runtime abstraction of sorts (neither the programmer nor the compiler is choosing the actual addresses in memory to alloc/dealloc), so at what point on the continuum of C++-style RAII, Obj-C RC, Swift ARC and so on would you say "garbage collection" comes into play? Is it just one of those "I know it when I see it" type things?


Any time :)

So, one of the reasons this is a bit muddy is that historically speaking, it was much more clear. You had languages which did some kind of "automatic" memory management, if that was through a refcount system of some kind (Like Python was (is?)), or a tracing GC (like Lisp was/is, or Ruby, whatever). And then you had languages which had some sort of "manual" memory management, where you explicitly allocate and free heap memory. I think that the real distinction here is "does runtime state decide if something gets deallocated"? If so, then that's garbage collected. If not, then it's "manual".

But languages like Rust and Swift sort of bend this, and I see them as coming from each side, looking in. So Swift is like a GC'd language that's not _quite_ GC'd, while Rust is like a manually managed language that's not _quite_ manually managed. They feel a bit different.


Thanks again; very interesting stuff, and I feel like my understanding of the concept here is quite a bit better now. It'd make a good blog post, I think. (I'm probably the only person seeing this convo).

It's a great skill to be able to explain pretty complex topics in "layman's" terms; I don't think I'm alone in having the experience of posing a question to an expert, getting a jargon-heavy answer that takes 30min of Googling to parse (or a condescending one that assumes I'm unfamiliar with basic CS concepts), and left feeling foolish for asking. So kudos to you! :)


Thanks! I've worked very hard at it; it's why my job is docs :) Have a great night (or whatever your time zone is)!


Good guess; EDT here/Boston. :) You too!


Brooklyn here. I may show up at the Boston Rust meetup over the next few months!


> It is easy to pick up, understand and use. But it is also very error prone

These two sentences are badly in conflict. How can you say you “understand” something if you find yourself repeatedly making mistakes when you use it?


Compare a hammer and nails to a nailgun with various safety mechanisms. Which one do you think is easier to understand, and which is more prone to sore fingers?


> Which one do you think is easier to understand?

For a toolmaker, the hammer. For a user, the nailgun.

Also, there's a qualitative difference between physical tools and programming languages. The design of a physical tool can only lessen the likelihood of an accident and/or the seriousness of its consequences, but never entirely rule them out - that's why they're called “accidents”. On the other hand, programming languages can be designed to treat logical errors as invalid code. A program that doesn't compile is completely guaranteed not to corrupt your data, reveal your passwords to hackers, etc.


What if you can't understand the logic of the compiler, and therefore aren't able to write any code in the language at all? Sure it's not very error-prone, but it's also not very useful.

I'm a big fan of Rust precisely because of its safety features. Even if you don't need manual memory management there are huge benefits to resource management safety that no other language can provide. But I still recognize that there's a trade-off here.


Not who you're responding to, but go's ethos and tooling all seem oriented towards There's One Way To Do It, no cleverness, nothing like C++ where one shop's parts of the language used differs greatly from another's. That makes programmers much easier to swap in and out in a large project. I have no idea how productive go is and can't speak to it, but everything I have heard about go makes it sound like making programmers fungible is a design goal.


I am more productive in Go and C++ than in Rust. But for sure i am more productive in Rust than in C.


What's your mileage in Rust compared to C++? I found as soon as I got in the borrow-checker mindset I spent less and less time thinking about ownership and lifetimes to the point where I don't care anymore. I spend more time thinking about my program and its architecture (which I'd spend in any language) than thinking about Rust itself.

What would you say are exactly the pain points of productivity for you when using Rust?

My biggest gripes with Rust are macros (they're so useful but sooo hard at the same time compared to Lispy languages, where you don't have to fight the macro system as much due to their simpler syntax), compile times (which I hope will get better) and the fact that even at 1.x the the language and std still don't feel mature enough.


What about memory safety? are c++ smart pointers equivalent to rust in safety ?


Yes, but they are only checkable at runtime in C++.


Only assuming that you never use references (or raw pointers), which depending on your needs may be unavoidable.


So please tell us what is a productive language?

At least any argument why Go is not productive, why its productivity is an illusion.


> At least any argument why Go is not productive, why its productivity is an illusion.

- generic code only via go generate and interface {}

- verbose error handling

- the way dependencies are handled

- no language support for functional paradigms

- nil pointer semantics in interfaces

- very thin type system

Go would be a great language in the mid-90's, nowadays given the features adopted in the mainstream it feels too little.

For me, the positive thing is that it may lure developers that would use C to use Go instead, and in the process discover that their use case is perfectly doable with a memory safe language.


I've done one successful small/medium size TCP server project in Golang. Primarily programming in C++.

- Generics were absolutely no issue. - Golang error handling may be verbose, but it also ensures errors locally visible and are handled where they occur. Just need to ensure all return values are handled. I much prefer it to C++, where you can never tell from local context what might throw and what won't.

Generally I found it to be very productive compared to at least C++ and Java.

Two aspects of Golang were a huge productivity boost for the project in question: struct automatic (de)serialization to JSON, XML, etc. was very nice. Goroutines and channels simplified the code.


> I like this a lot, it's always tempting to think that we can create a perfect language that would be the best in all situations but we can't.

We may one day, but we should be uncompromising on a good, no nonsense. That varies for everyone but I think a lot of people including myself feel something Pythonic would be that syntax. Implementations can work around syntaxes, but it's important that the programmer comes first rather than the machine.

JIT'd and compiled Python would fit this need well. We already have both but it could be improved upon.


No complaints about the meat of the article, just with two introductory remarks:

> For example, Ruby famously values Developer Happiness and that value has impacted Ruby’s features.

Really? Every time I give Ruby another chance (admittedly, not too often), I end up feeling angry. I can stare at five lines of code for several minutes, and have no idea what it will do. Not even Haskell.

> Ruby also protects you from segmentation fault errors. But to do so it uses a garbage collector. This is great, but it has a big impact on your program’s speed.

Is garbage collection the main culprit? Not duck typing? Not dynamic metaprogramming? I reckon performing a hashtable lookup during every single method call has a bigger effect on performance than garbage collection.


> I reckon performing a hashtable lookup during every single method call has a bigger effect on performance than garbage collection.

Well, a high-performance Ruby VM will perform inline caching to mitigate this. And if the VM isn't a high-performance VM (i.e. it's interpreted), then the overhead of the interpreter dispatch loop dominates everything.


The notion that the overhead of interpreter dispatch dominates everything else (and thus the most focus should be placed on optimizing dispatch) was formed with experiments against microarchitectures that are fairly primitive by current standards.

Earlier indirect branch predictors didn't take much execution history into account, mostly associating a predicted branch target with the address of a branch instruction. With a predictor of this kind, using a distinct branch instruction for distinct execution paths dramatically improves the amount of context available to the branch predictor. The same remains true if you add a small amount of context to the predictor.

Over time, conditional branch predictors incorporated more branch history, e.g. the various designs like OGEHL / TAGE based on associating a branch instruction with multiple branch histories whose lengths form a geometric series. The same trick was quite successful when applied to indirect branches, although it is more difficult to implement because the result of a prediction is an address rather than a single bit indicating the branch direction.

You can see evaluations of various benchmarks against both actual Intel processors and simulations of the leading academic branch predictor designs here:

https://hal.inria.fr/hal-01100647/document

On Haswell and later (and with their model of ITTAGE) the mispredictions encountered by a switch-based interpreter loop are negligible.

That being said, I would still write an interpreter in assembly myself, because the code generators found in traditional compilers do a poor job of generating code for interpreters, and you can do a better job of deciding what work goes on the inline fast path and what goes on a slow path. When you're writing an interpreter by hand, you might as well use separate indirect branches for dispatch because there's no impedance mismatch like there is in C/C++.


"because the code generators found in traditional compilers do a poor job of generating code for interpreters,"

Are you referring to the assembler in the GNU toolchaine or? Could you elaborate on this sentiment? Thanks.


I mean "code generator" as in the backend of a compiler that comes after most optimizations have been done. In particular, most common register allocators and schedulers are designed to optimize code that looks much different than an interpreter loop, and they generally do a poor job on interpreters.


I'm admittedly not very familiar with compiler backends - especially not JIT compilers. But how does this optimization even work if I'm constantly modifying the method tables of objects at runtime? If I understand correctly, ORMs and Web frameworks for dynamic languages do this sort of thing all the time.


In a JIT for a dynamic language, you generally have to predicate your more aggressive optimizations on some runtime condition, e.g. "this variable is actually an integer", or "this object has the same method layout as other objects did before". These conditions are checked before relying on the optimization, and if they fail, the generated code jumps into a runtime that may decide to use an interpreter, generate deoptimized code, etc.


This actually sounds more complicated thank thinking ahead how to define your abstraction in more precise terms. (So that you don't need to perform optimistic optimizations that you might later need to roll back.)


I don't understand, who should define such abstractions? If you're the JIT developer and you define a value encoding that supports all possible values (so that the JITed function never has to be rolled back), then you can't really optimize.

For example, if the JIT has the code "function(a, b) { return a+b; }", and it sees that the function has been called 50 times with a and b as integers, it can generate a machine code implementation that is just a couple of instructions adding two registers.

If you keep the abstractions wide enough to support other values, you can never achieve this efficiency.


> I don't understand, who should define such abstractions?

The language designer.

> If you're the JIT developer and you define a value encoding that supports all possible values (so that the JITed function never has to be rolled back), then you can't really optimize.

Obviously, the solution is to know the types of values ahead of time. This is precisely where a language with more precise abstractions can make the lives of implementors easier.


Well, yes, but the whole point is to make the lives of the end programmers easier, not of the language implementors. If you want to avoid forcing people to manually tag each variable, the implementors either need to write an optimistic JIT or a really good type inference engine.


More precise abstractions benefit end programmers too:

(0) They increase the effectiveness of code as a communication medium between programmers. To use an analogy: If I tell you a story, you don't need to hire actors to enact it, just to understand the plot. You can just use your knowledge of the English language, right? Imagine if we could do the same with code: programmers conversing in code, because they understand it natively, without having to “interpret” it (in a REPL, using a test suite, etc.).

(1) They decrease the burden of anticipating, preventing and handling things that may go wrong. Again, to use an analogy: Defensive programming in a language with imprecise abstractions is like spending 75% of a construction project's budget on buying insurance.

The fact that precise abstractions usually lend themselves to simpler and more elegant implementations than imprecise abstractions, well, it is just a nice additional benefit.


Clearly there are millions of end programmers who disagree with you, since they could be using a language with more precise abstractions and yet they choose not to. As one of them, my claim is that you need to eliminate the burden of manually tagging each value, because that effort usually isn't worth it. And eliminating that burden while keeping precise abstractions is not an easy task for implementors.


They're generally operating on the assumption that there's some amount of setup that you do initially, maybe the first time you try to call methods on your magic ORM object, that will set up the inheritance for that class and inject the appropriate methods, and then this will mostly stay static for the remainder of the process lifetime. This seems to me like a generally reasonable assumption.


I don't think this scales to systems with more than, say, 50 tables. In an ERP system with dozens of thousands of tables and views, and which supports adding new functionality to a running application server, you can't assume that the database schema will remain fixed throughout the lifetime of the application server process.


> Every time I give Ruby another chance (admittedly, not too often), I end up feeling angry.

I use Ruby every day at work. I end most days feeling angry. You never have any real idea what is going on due to the layers and layers of metaprogramming. Nil-pointer errors abound, duck typing is idiomatic, inheritance is a thing, and the minefield of mutability is everywhere. Concurrency is nigh impossible. Ruby looks pretty and simple at first glance, but the deeper and deeper you look, the more you realize how rotten it is at the core. :(

/rant


Really? Every time I give Ruby another chance (admittedly, not too often), I end up feeling angry. I can stare at five lines of code for several minutes, and have no idea what it will do. Not even Haskell.

I mostly feel the same, but the reality is that Ruby is used by plenty of programmers when they have the choice, even if they use something else at work.

Is garbage collection the main culprit?

The author didn't say it was.


Matz has said that the overall guiding principle of Ruby is "developer happiness." While some people can disagree with this, it's a pretty standard things Rubyists say, because Ruby does make many choices that prioritize their happiness over other things.


> It's a pretty standard things Rubyists say, Ruby does make many choices that prioritize their happiness over other things. [emphasis mine]

Isn't this true of most languages with a sizable user base? Most language designers seek to “please their constituency” to some extent. Do you know any language with a feature that was put into it in spite of opposition from the majority of its users?


I mean, no language wants to make their users unhappy, but it's about the priorities of the language designers. So for example, if you ask Rust's core team what we focus on, we say:

  1. safety
  2. speed
  3. concurrency
If you ask Matz about Ruby, he says

   > I hope to see Ruby help every programmer in the world to be productive,
   > and to enjoy programming, and to be happy. That is the primary
   > purpose of Ruby language.
https://www.youtube.com/watch?v=oEkJvvGEtB4

As a good example of this, a lot of programmers are much happier with a GC than with manual memory management, or a Rust-like system. But Rust's core values mean that it can't have a GC, even though that might make some people happy.


> a lot of programmers are much happier with a GC than with manual memory management

I don't disagree. I'm happier when I don't have to worry about irrelevant things, like manual memory management. And of course I'm happier when I can achieve more results with less effort, like when there is a libary or framework that does what I need. This is only natural.

But not everything about Ruby is a happiness or productivity booster:

(0) Ruby doesn't lend itself to writing code that documents its own structure very well, which is a time sink when the code changes faster than your ability to document the changes. [No, tests aren't documentation. At best, they only document your intentions, not the actual code.]

(1) Ruby makes it unnecessarily difficult to perform basic sanity checks, like ensuring that you haven't forgotten any `when` branches in a `case` statement. The smallest testable unit is a method. Try unit-testing a loop invariant.

(2) As if the above technical deficiency weren't enough, culturally, Ruby pushes towards more cumbersome forms of testing [behavior-driven development], rather than less [property-based testing].

Note that none of these disadvantages has anything to do with the fact Ruby uses a garbage collector.


Maybe this is off topic, but while reading this article it occurred to me that wouldn't it be useful if the ownership transfer is syntax highlighted?

e.g. in

  let new_owner = original_owner;
  println!("{}", original_owner);
we could have original_owner on the second line have a different color signifying it doesn't own anything.

Or, does such a syntax highlighter already exist?


Atom with linter-rust highlights compile errors: http://i.imgur.com/ac7FTpW.png

Or do you have a more specific visualisation for moves/ownership in mind?


Didn't know about linter-rust. Thanks for sharing.

It didn't occur to me that: ownership transfer means variable cannot be used, so if it's used means there's an error, so checking for error itself gives you the information that you cannot use it. So highlighting error can be used instead of the specific ownership transfer.

I don't know of any other specific cases where there's no error, but it would be helpful to have a different color for certain ownership semantics.


That syntax highlighter would basically have to be attached to a compiler. If a type is Copy, then it follows move semantics instead of copy semantics. Plus, ownership can be transferred through things like method invocations as well, depending on the type signature of the method.


A bunch of languages are moving towards integrating IDE tools into the compiler with some kind of API. This allows for on-the-fly type checking, better syntax highlighting, better autocompletion, automatically generating bits of boilerplate, instantly testing a block of code, etc. For a language with a great example of this, check out Idris's IDE protocol: http://docs.idris-lang.org/en/latest/reference/ide-protocol..... The way this works helps create an awesome feedback loop where the editor _is_ the REPL. Something like that would be a great addition to Rust and would definitely help improve the overall tooling for the language.


There is a fairly extensive plan for building out IDE support for Rust: https://www.rust-lang.org/ides.html


"Of course, Rust developers also wants people to write programs in Rust. So we can declare things to be mutable if we really need to." - I laughed so hard on this more than I should have :D Really a good one!


Could someone explain this - "Ruby also protects you from segmentation fault errors. But to do so it uses a garbage collector."?

How does GC protect you from a segfault?

Surely trying that doesn't help you when you attempt to access the 5th element of an array with only two elements. Maybe its just confusing that the author used that as an example. I'm guessing aside form a dangling pointer GC doesn't offer much protection form a segfault?


Of course, garbage collection per se doesn't protect you from segfaults. For example, there exist garbage collectors for C. They reclaim unused memory, but you can still cause segfaults if you want to (or are careless).

Automatic memory management (whether done at runtime with a garbage collector, or at compile time as in Rust) frees you from logical memory errors by making them inexpressible. But automatic memory management doesn't guarantee that segfaults won't happen - it only guarantees that they won't matter to the programmer. For example, a garbage collector could deliberately trigger segfaults (by `mprotect()`ing all managed memory) to forcibly pause the mutator during a garbage collection cycle. Every managed thread must install a `SIGSEGV` handler that waits until the collection process is done.


"For example, a garbage collector could deliberately trigger segfaults (by `mprotect()`ing all managed memory) to forcibly pause the mutator during a garbage collection cycle. Every managed thread must install a `SIGSEGV` handler that waits until the collection process is done."

Interesting, is this actually a common implementation pattern for languages that have a GC built into their run time?


I don't think the exact thing you describe is common, but plenty of garbage collectors use memory protection to implement the write barrier. This is particularly useful when integrating with arbitrary code you don't control, since it will also be affected by memory protection.


No idea. I haven't read the source code of any serious, production GC. I've only written toy ones.


A segfault implies a pointer to an unmapped page in memory. An array out of bounds error isn't the same class of error. The former implies that what pointers can point at isn't constrained; instead of pointing at unmapped page, it could point at the inside of some sensitive data structure (think: web response buffer pointing at database credentials). The latter implies that indirections are checked; this is a very different guarantee.


Thanks, could you explain the "indirections" in this context? I haven't heard this before.


Array accesses are implicit indirections. The base, index and element size are used to compute the address of a location which is then indirected to store or load. Without range checking, array accesses can be used to read or write arbitrary locations in memory, just like pointers.


Ah ok right, that makes perfect sense. Thanks.


> I'm guessing aside form a dangling pointer GC doesn't offer much protection form a segfault?

Correct. By using GC and banning free() you avoid use-after-free bugs, but not out-of-bounds accesses.

Out of bounds accesses have to be forbidden in some other fashion; most languages do bounds-checking and throw an exception rather than trying to access the memory and getting a segfault (or, worse, reading some random other object's memory).


Thanks


Mixing some C++ into Standard ML is not necessarily a good idea, like mixing a bit of shit into a bucket of honey.




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

Search: